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:
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# struct MyRule < Ameba::Rule::Base
|
|
|
|
# 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.
|
2017-11-15 18:49:09 +00:00
|
|
|
#
|
2017-11-07 21:50:25 +00:00
|
|
|
abstract struct 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)
|
|
|
|
source.tap { |s| test s }
|
|
|
|
end
|
|
|
|
|
2017-11-15 18:49:09 +00:00
|
|
|
# Returns a name of this rule, which is basically a class name.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# struct MyRule < Ameba::Rule::Base
|
|
|
|
# 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.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# struct MyGroup::MyRule < Ameba::Rule::Base
|
|
|
|
# # ...
|
|
|
|
# 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
|
|
|
|
|
2018-06-10 21:15:12 +00:00
|
|
|
macro issue_for(*args)
|
|
|
|
source.add_issue self, {{*args}}
|
|
|
|
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
|
|
|
|
|
|
|
macro inherited
|
|
|
|
protected def self.path_to_source_file
|
|
|
|
__FILE__
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns documentation for this rule if any.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# module Ameba
|
|
|
|
# # This is a test rule.
|
|
|
|
# # Does nothing.
|
|
|
|
# struct MyRule < Ameba::Rule::Base
|
|
|
|
# def test(source)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# MyRule.parsed_doc # => "This is a test rule.\nDoes nothing."
|
|
|
|
# ```
|
|
|
|
def self.parsed_doc
|
|
|
|
source = File.read(path_to_source_file)
|
|
|
|
nodes = Crystal::Parser.new(source).tap(&.wants_doc = true).parse
|
2021-01-11 18:13:58 +00:00
|
|
|
type_name = rule_name.split('/').last?
|
2018-12-08 20:52:32 +00:00
|
|
|
DocFinder.new(nodes, type_name).doc
|
|
|
|
end
|
|
|
|
|
|
|
|
# :nodoc:
|
|
|
|
private class DocFinder < Crystal::Visitor
|
|
|
|
getter doc : String?
|
|
|
|
getter type_name : String?
|
|
|
|
|
|
|
|
def initialize(nodes, @type_name)
|
|
|
|
self.accept(nodes)
|
|
|
|
end
|
|
|
|
|
|
|
|
def visit(node : Crystal::ASTNode)
|
|
|
|
return false if @doc
|
|
|
|
|
|
|
|
if node.responds_to?(:name) &&
|
|
|
|
(name = node.name) &&
|
|
|
|
name.is_a?(Crystal::Path) &&
|
|
|
|
name.names.last? == @type_name
|
|
|
|
@doc = node.doc
|
|
|
|
end
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
|
|
|
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
|
2018-02-02 20:11:18 +00:00
|
|
|
Base.subclasses
|
2017-11-07 21:50:25 +00:00
|
|
|
end
|
2017-10-30 20:00:01 +00:00
|
|
|
end
|