From 65f7db0a492598548249778bb8ff69937a5801e8 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Thu, 9 Jan 2025 11:08:35 -0500 Subject: [PATCH] Add `Typing/MacroCallArgumentTypeRestriction` (#521) --- ...cro_call_argument_type_restriction_spec.cr | 70 ++++++++++++++ .../macro_call_argument_type_restriction.cr | 94 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 spec/ameba/rule/typing/macro_call_argument_type_restriction_spec.cr create mode 100644 src/ameba/rule/typing/macro_call_argument_type_restriction.cr diff --git a/spec/ameba/rule/typing/macro_call_argument_type_restriction_spec.cr b/spec/ameba/rule/typing/macro_call_argument_type_restriction_spec.cr new file mode 100644 index 000000000..47a8f92e9 --- /dev/null +++ b/spec/ameba/rule/typing/macro_call_argument_type_restriction_spec.cr @@ -0,0 +1,70 @@ +require "../../../spec_helper" + +module Ameba::Rule::Typing + describe MacroCallArgumentTypeRestriction do + subject = MacroCallArgumentTypeRestriction.new + + it "passes if macro call args have type restrictions" do + expect_no_issues subject, <<-CRYSTAL + class Greeter + getter name : String? + class_getter age : Int32 = 0 + setter tasks : Array(String) = [] of String + class_setter queue : Array(Int32)? + property task_mutex : Mutex = Mutex.new + class_property asdf : String + end + + record Task, + cmd : String, + args : Array(String) = %w[] + CRYSTAL + end + + it "fails if a macro call arg doesn't have a type restriction" do + expect_issue subject, <<-CRYSTAL + class Greeter + getter name + # ^^^^ error: Argument should have a type restriction + getter :age + # ^^^^ error: Argument should have a type restriction + getter "height" + # ^^^^^^^^ error: Argument should have a type restriction + end + CRYSTAL + end + + it "passes if a record call arg with a default value doesn't have a type restriction" do + expect_no_issues subject, <<-CRYSTAL + record Task, + cmd : String, + args = %[] + CRYSTAL + end + + context "properties" do + context "#default_value" do + rule = MacroCallArgumentTypeRestriction.new + rule.default_value = true + + it "fails if a macro call arg with a default value doesn't have a type restriction" do + expect_issue rule, <<-CRYSTAL + class Greeter + getter name = "Kenobi" + # ^^^^ error: Argument should have a type restriction + end + CRYSTAL + end + + it "fails if a record call arg with default value doesn't have a type restriction" do + expect_issue rule, <<-CRYSTAL + record Task, + cmd : String, + args = %[] + # ^^^^ error: Argument should have a type restriction + CRYSTAL + end + end + end + end +end diff --git a/src/ameba/rule/typing/macro_call_argument_type_restriction.cr b/src/ameba/rule/typing/macro_call_argument_type_restriction.cr new file mode 100644 index 000000000..fab1e2d82 --- /dev/null +++ b/src/ameba/rule/typing/macro_call_argument_type_restriction.cr @@ -0,0 +1,94 @@ +module Ameba::Rule::Typing + # A rule that enforces call arguments to specific macros have a type restriction. + # By default these macros are: `(class_)getter/setter/property(?/!)` and `record`. + # + # For example, these are considered invalid: + # + # ``` + # class Greeter + # getter name + # getter age = 0.days + # getter :height + # end + # + # record Task, + # cmd = "", + # args = %w[] + # ``` + # + # And these are considered valid: + # + # ``` + # class Greeter + # getter name : String? + # getter age : Time::Span = 0.days + # getter height : Float64? + # end + # + # record Task, + # cmd : String = "", + # args : Array(String) = %w[] + # ``` + # + # The `DefaultValue` configuration option controls whether this rule applies to + # call arguments that have a default value. + # + # YAML configuration example: + # + # ``` + # Typing/MacroCallArgumentTypeRestriction: + # Enabled: false + # DefaultValue: false + # MacroNames: + # - getter + # - getter? + # - getter! + # - class_getter + # - class_getter? + # - class_getter! + # - setter + # - setter? + # - setter! + # - class_setter + # - class_setter? + # - class_setter! + # - property + # - property? + # - property! + # - class_property + # - class_property? + # - class_property! + # - record + # ``` + class MacroCallArgumentTypeRestriction < Base + properties do + since_version "1.7.0" + description "Recommends that call arguments to certain macros have type restrictions" + enabled false + default_value false + macro_names %w[ + getter getter? getter! class_getter class_getter? class_getter! + setter setter? setter! class_setter class_setter? class_setter! + property property? property! class_property class_property? class_property! + record + ] + end + + MSG = "Argument should have a type restriction" + + def test(source, node : Crystal::Call) + return unless node.name.in?(macro_names) + + node.args.each do |arg| + case arg + when Crystal::Assign + next unless default_value? + + issue_for arg.target, MSG, prefer_name_location: true + when Crystal::Var, Crystal::Call, Crystal::StringLiteral, Crystal::SymbolLiteral + issue_for arg, MSG, prefer_name_location: true + end + end + end + end +end