2017-11-07 21:50:25 +00:00
|
|
|
module Ameba::Rule
|
2018-02-02 20:11:18 +00:00
|
|
|
# List of names of the special rules, which
|
|
|
|
# behave differently than usual rules.
|
2018-02-02 08:52:21 +00:00
|
|
|
SPECIAL = [
|
2018-06-16 11:50:59 +00:00
|
|
|
Lint::Syntax.rule_name,
|
|
|
|
Lint::UnneededDisableDirective.rule_name,
|
2018-02-02 08:52:21 +00:00
|
|
|
]
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# Represents a base of all rules. In other words, all rules
|
|
|
|
# inherits from this struct:
|
|
|
|
#
|
|
|
|
# ```
|
2021-01-18 15:45:35 +00:00
|
|
|
# class MyRule < Ameba::Rule::Base
|
2017-11-15 18:49:09 +00:00
|
|
|
# def test(source)
|
|
|
|
# if invalid?(source)
|
2018-06-10 21:15:12 +00:00
|
|
|
# issue_for line, column, "Something wrong."
|
2017-11-15 18:49:09 +00:00
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# private def invalid?(source)
|
|
|
|
# # ...
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# ```
|
|
|
|
#
|
|
|
|
# Enforces rules to implement an abstract `#test` method which
|
|
|
|
# is designed to test the source passed in. If source has issues
|
2018-06-10 21:15:12 +00:00
|
|
|
# that are tested by this rule, it should add an issue.
|
2021-01-18 15:45:35 +00:00
|
|
|
abstract class Base
|
2017-11-23 17:49:45 +00:00
|
|
|
include Config::RuleConfig
|
2017-11-13 21:20:22 +00:00
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# This method is designed to test the source passed in. If source has issues
|
2018-06-10 21:15:12 +00:00
|
|
|
# that are tested by this rule, it should add an issue.
|
2020-03-25 16:21:07 +00:00
|
|
|
#
|
|
|
|
# Be default it uses a node visitor to traverse all the nodes in the source.
|
|
|
|
# Must be overriten for other type of rules.
|
|
|
|
def test(source : Source)
|
|
|
|
AST::NodeVisitor.new self, source
|
|
|
|
end
|
2017-10-30 20:00:01 +00:00
|
|
|
|
2018-05-03 15:57:47 +00:00
|
|
|
def test(source : Source, node : Crystal::ASTNode, *opts)
|
2017-11-06 18:54:58 +00:00
|
|
|
# can't be abstract
|
2017-10-31 22:47:29 +00:00
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# A convenient addition to `#test` method that does the same
|
|
|
|
# but returns a passed in `source` as an addition.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# source = MyRule.new.catch(source)
|
|
|
|
# source.valid?
|
|
|
|
# ```
|
2017-10-30 20:00:01 +00:00
|
|
|
def catch(source : Source)
|
2021-01-17 20:31:53 +00:00
|
|
|
source.tap { test source }
|
2017-10-30 20:00:01 +00:00
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# Returns a name of this rule, which is basically a class name.
|
|
|
|
#
|
|
|
|
# ```
|
2021-01-18 15:45:35 +00:00
|
|
|
# class MyRule < Ameba::Rule::Base
|
2017-11-15 18:49:09 +00:00
|
|
|
# def test(source)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# MyRule.new.name # => "MyRule"
|
|
|
|
# ```
|
2017-10-30 20:00:01 +00:00
|
|
|
def name
|
2018-02-02 20:11:18 +00:00
|
|
|
{{@type}}.rule_name
|
2017-11-22 06:44:29 +00:00
|
|
|
end
|
|
|
|
|
2018-06-18 07:25:06 +00:00
|
|
|
# Returns a group this rule belong to.
|
|
|
|
#
|
|
|
|
# ```
|
2021-01-18 15:45:35 +00:00
|
|
|
# class MyGroup::MyRule < Ameba::Rule::Base
|
2018-06-18 07:25:06 +00:00
|
|
|
# # ...
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# MyGroup::MyRule.new.group # => "MyGroup"
|
|
|
|
# ```
|
|
|
|
def group
|
|
|
|
{{@type}}.group_name
|
|
|
|
end
|
|
|
|
|
2017-12-18 11:06:19 +00:00
|
|
|
# Checks whether the source is excluded from this rule.
|
|
|
|
# It searches for a path in `excluded` property which matches
|
|
|
|
# the one of the given source.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# my_rule.excluded?(source) # => true or false
|
|
|
|
# ```
|
|
|
|
def excluded?(source)
|
|
|
|
excluded.try &.any? do |path|
|
2018-05-29 10:19:00 +00:00
|
|
|
source.matches_path?(path) ||
|
2021-01-11 18:13:58 +00:00
|
|
|
Dir.glob(path).any? { |glob| source.matches_path?(glob) }
|
2017-12-18 11:06:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-02-02 20:11:18 +00:00
|
|
|
# Returns true if this rule is special and behaves differently than
|
|
|
|
# usual rules.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# my_rule.special? # => true or false
|
|
|
|
# ```
|
|
|
|
def special?
|
2021-01-17 17:12:10 +00:00
|
|
|
name.in?(SPECIAL)
|
2018-02-02 20:11:18 +00:00
|
|
|
end
|
|
|
|
|
2019-10-27 20:15:04 +00:00
|
|
|
def ==(other)
|
2021-01-17 17:12:10 +00:00
|
|
|
name == other.try(&.name)
|
2019-10-27 20:56:53 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def hash
|
|
|
|
name.hash
|
2019-10-27 20:15:04 +00:00
|
|
|
end
|
|
|
|
|
2021-10-26 19:02:17 +00:00
|
|
|
macro issue_for(*args, **kwargs, &block)
|
|
|
|
source.add_issue(self, {{*args}}, {{**kwargs}}) {{block}}
|
2018-06-10 21:15:12 +00:00
|
|
|
end
|
|
|
|
|
2018-02-02 20:11:18 +00:00
|
|
|
protected def self.rule_name
|
2021-01-11 18:13:58 +00:00
|
|
|
name.gsub("Ameba::Rule::", "").gsub("::", '/')
|
2017-10-30 20:00:01 +00:00
|
|
|
end
|
2017-11-01 10:49:03 +00:00
|
|
|
|
2018-06-18 07:25:06 +00:00
|
|
|
protected def self.group_name
|
2021-01-11 18:13:58 +00:00
|
|
|
rule_name.split('/')[0...-1].join('/')
|
2018-06-18 07:25:06 +00:00
|
|
|
end
|
|
|
|
|
2017-11-07 21:50:25 +00:00
|
|
|
protected def self.subclasses
|
2017-11-01 10:49:03 +00:00
|
|
|
{{ @type.subclasses }}
|
|
|
|
end
|
2018-12-08 20:52:32 +00:00
|
|
|
|
2021-04-15 14:26:49 +00:00
|
|
|
protected def self.abstract?
|
|
|
|
{{ @type.abstract? }}
|
|
|
|
end
|
|
|
|
|
|
|
|
protected def self.inherited_rules
|
|
|
|
subclasses.each_with_object([] of Base.class) do |klass, obj|
|
|
|
|
klass.abstract? ? obj.concat(klass.inherited_rules) : (obj << klass)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-28 23:23:26 +00:00
|
|
|
private macro read_rule_doc(filepath = __FILE__)
|
|
|
|
{{ run("../../read_rule_doc",
|
|
|
|
@type.name.split("::").last,
|
|
|
|
filepath
|
|
|
|
).chomp.stringify }}.presence
|
2018-12-08 20:52:32 +00:00
|
|
|
end
|
|
|
|
|
2022-10-28 23:23:26 +00:00
|
|
|
macro inherited
|
|
|
|
# Returns documentation for this rule, if there is any.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# module Ameba
|
|
|
|
# # This is a test rule.
|
|
|
|
# # Does nothing.
|
|
|
|
# class MyRule < Ameba::Rule::Base
|
|
|
|
# def test(source)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# MyRule.parsed_doc # => "This is a test rule.\nDoes nothing."
|
|
|
|
# ```
|
|
|
|
class_getter parsed_doc : String? = read_rule_doc
|
2018-12-08 20:52:32 +00:00
|
|
|
end
|
2017-10-30 20:00:01 +00:00
|
|
|
end
|
2017-11-07 21:50:25 +00:00
|
|
|
|
2018-06-18 07:25:06 +00:00
|
|
|
# Returns a list of all available rules.
|
2017-11-15 18:49:09 +00:00
|
|
|
#
|
|
|
|
# ```
|
2018-06-18 07:25:06 +00:00
|
|
|
# Ameba::Rule.rules # => [Rule1, Rule2, ....]
|
2017-11-15 18:49:09 +00:00
|
|
|
# ```
|
2017-11-07 21:50:25 +00:00
|
|
|
def self.rules
|
2021-04-15 14:26:49 +00:00
|
|
|
Base.inherited_rules
|
2017-11-07 21:50:25 +00:00
|
|
|
end
|
2017-10-30 20:00:01 +00:00
|
|
|
end
|