shard-ameba/src/ameba/rule/base.cr

198 lines
4.4 KiB
Crystal
Raw Normal View History

2017-11-07 21:50:25 +00:00
module Ameba::Rule
# List of names of the special rules, which
# behave differently than usual rules.
SPECIAL = [
Lint::Syntax.rule_name,
Lint::UnneededDisableDirective.rule_name,
]
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.
#
# 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
def test(source : Source, node : Crystal::ASTNode, *opts)
# can't be abstract
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
{{@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) ||
Dir.glob(path).any? { |glob| source.matches_path?(glob) }
2017-12-18 11:06:19 +00:00
end
end
# Returns true if this rule is special and behaves differently than
# usual rules.
#
# ```
# my_rule.special? # => true or false
# ```
#
def special?
SPECIAL.includes? name
end
def ==(other)
name == other.try &.name
end
def hash
name.hash
end
2018-06-10 21:15:12 +00:00
macro issue_for(*args)
source.add_issue self, {{*args}}
end
protected def self.rule_name
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
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
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
Base.subclasses
2017-11-07 21:50:25 +00:00
end
2017-10-30 20:00:01 +00:00
end