Remove dsl & refactor ast visitors

closes #4
This commit is contained in:
Vitalii Elenhaupt 2017-11-01 00:47:29 +02:00
parent 6cef83f9a9
commit f1e462cc86
No known key found for this signature in database
GPG key ID: 7558EF3A4056C706
9 changed files with 142 additions and 122 deletions

27
src/ameba/ast.cr Normal file
View file

@ -0,0 +1,27 @@
require "compiler/crystal/syntax/*"
module Ameba
NODE_VISITORS = [
Unless,
Call,
]
{% for name in NODE_VISITORS %}
class {{name}}Visitor < Crystal::Visitor
@rule : Rule
@source : Source
def initialize(@rule, @source)
@source.ast.accept self
end
def visit(node : Crystal::ASTNode)
true
end
def visit(node : Crystal::{{name}})
@rule.test @source, node
end
end
{% end %}
end

View file

@ -1,32 +0,0 @@
module Ameba
macro rule(name, &block)
module Ameba::Rules
struct {{name.id}} < Rule
def test(source)
{{block.body}}
end
end
end
end
macro visitor(name, node, &block)
module Ameba::Rules
class {{name.id}}Visitor < Crystal::Visitor
@rule : Rule
@source : Source
def initialize(@rule, @source)
@source.ast.accept self
end
def visit(node : Crystal::ASTNode)
true
end
def visit(node : {{node.id}})
{{block.body}}
end
end
end
end
end

View file

@ -10,6 +10,10 @@ module Ameba
abstract struct Rule abstract struct Rule
abstract def test(source : Source) abstract def test(source : Source)
def test(source : Source, node : Crystal::ASTNode)
raise "Unimplemented"
end
def catch(source : Source) def catch(source : Source)
source.tap { |s| test s } source.tap { |s| test s }
end end

View file

@ -1,3 +1,4 @@
module Ameba::Rules
# A rule that disallows comparison to booleans. # A rule that disallows comparison to booleans.
# #
# For example, these are considered invalid: # For example, these are considered invalid:
@ -10,17 +11,19 @@
# This is because these expressions evaluate to `true` or `false`, so you # This is because these expressions evaluate to `true` or `false`, so you
# could get the same result by using either the variable directly, or negating # could get the same result by using either the variable directly, or negating
# the variable. # the variable.
struct ComparisonToBoolean < Rule
Ameba.rule ComparisonToBoolean do |source| def test(source)
ComparisonToBooleanVisitor.new self, source CallVisitor.new self, source
end end
Ameba.visitor ComparisonToBoolean, Crystal::Call do |node| def test(source, node : Crystal::Call)
if %w(== != ===).includes?(node.name) && ( if %w(== != ===).includes?(node.name) && (
node.args.first?.try &.is_a?(Crystal::BoolLiteral) || node.args.first?.try &.is_a?(Crystal::BoolLiteral) ||
node.obj.is_a?(Crystal::BoolLiteral) node.obj.is_a?(Crystal::BoolLiteral)
) )
@source.error @rule, node.location.try &.line_number, source.error self, node.location.try &.line_number,
"Comparison to a boolean is pointless" "Comparison to a boolean is pointless"
end end
end end
end
end

View file

@ -1,8 +1,12 @@
module Ameba::Rules
# A rule that disallows lines longer than 79 symbols. # A rule that disallows lines longer than 79 symbols.
struct LineLength < Rule
Ameba.rule LineLength do |source| def test(source)
source.lines.each_with_index do |line, index| source.lines.each_with_index do |line, index|
next unless line.size > 79 next unless line.size > 79
source.error self, index + 1, "Line too long (#{line.size} symbols)" source.error self, index + 1,
"Line too long (#{line.size} symbols)"
end
end
end end
end end

View file

@ -1,8 +1,11 @@
module Ameba::Rules
# A rule that disallows trailing blank lines at the end of the source file. # A rule that disallows trailing blank lines at the end of the source file.
struct TrailingBlankLines < Rule
Ameba.rule TrailingBlankLines do |source| def test(source)
if source.lines.size > 1 && source.lines[-2, 2].join.strip.empty? if source.lines.size > 1 && source.lines[-2, 2].join.strip.empty?
source.error self, source.lines.size, source.error self, source.lines.size,
"Blank lines detected at the end of the file" "Blank lines detected at the end of the file"
end end
end end
end
end

View file

@ -1,8 +1,12 @@
# A rule that disallows trailing whitespace at the end of a line. module Ameba::Rules
# A rule that disallows trailing whitespaces.
Ameba.rule TrailingWhitespace do |source| struct TrailingWhitespace < Rule
def test(source)
source.lines.each_with_index do |line, index| source.lines.each_with_index do |line, index|
next unless line =~ /\s$/ next unless line =~ /\s$/
source.error self, index + 1, "Trailing whitespace detected" source.error self, index + 1,
"Trailing whitespace detected"
end
end
end end
end end

View file

@ -1,3 +1,4 @@
module Ameba::Rules
# A rule that disallows the use of an `else` block with the `unless`. # A rule that disallows the use of an `else` block with the `unless`.
# #
# For example, the rule considers these valid: # For example, the rule considers these valid:
@ -34,14 +35,16 @@
# :one # :one
# end # end
# ``` # ```
struct UnlessElse < Rule
Ameba.rule UnlessElse do |source| def test(source)
UnlessElseVisitor.new self, source UnlessVisitor.new self, source
end end
Ameba.visitor UnlessElse, Crystal::Unless do |node| def test(source, node : Crystal::Unless)
unless node.else.is_a?(Crystal::Nop) unless node.else.is_a?(Crystal::Nop)
@source.error @rule, node.location.try &.line_number, source.error self, node.location.try &.line_number,
"Favour if over unless with else" "Favour if over unless with else"
end end
end end
end
end

View file

@ -1,5 +1,3 @@
require "compiler/crystal/syntax/*"
module Ameba module Ameba
# An entity that represents a Crystal source file. # An entity that represents a Crystal source file.
# Has path, lines of code and errors reported by rules. # Has path, lines of code and errors reported by rules.
@ -13,13 +11,13 @@ module Ameba
pos : Int32?, pos : Int32?,
message : String message : String
getter lines : Array(String) getter lines : Array(String)?
getter errors = [] of Error getter errors = [] of Error
getter path : String? getter path : String?
getter content : String getter content : String
getter ast : Crystal::ASTNode?
def initialize(@content : String, @path = nil) def initialize(@content : String, @path = nil)
@lines = @content.split("\n")
end end
def error(rule : Rule, line_number : Int32?, message : String) def error(rule : Rule, line_number : Int32?, message : String)
@ -30,8 +28,14 @@ module Ameba
errors.empty? errors.empty?
end end
def lines
@lines ||= @content.split("\n")
end
def ast def ast
Crystal::Parser.new(@content).tap { |p| p.filename = @path }.parse @ast ||= Crystal::Parser.new(@content)
.tap { |p| p.filename = @path }
.parse
end end
end end
end end