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

View file

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

View file

@ -1,8 +1,11 @@
# A rule that disallows trailing blank lines at the end of the source file. module Ameba::Rules
# A rule that disallows trailing blank lines at the end of the source file.
Ameba.rule TrailingBlankLines do |source| struct TrailingBlankLines < Rule
if source.lines.size > 1 && source.lines[-2, 2].join.strip.empty? def test(source)
source.error self, source.lines.size, if source.lines.size > 1 && source.lines[-2, 2].join.strip.empty?
"Blank lines detected at the end of the file" source.error self, source.lines.size,
"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
source.lines.each_with_index do |line, index| def test(source)
next unless line =~ /\s$/ source.lines.each_with_index do |line, index|
source.error self, index + 1, "Trailing whitespace detected" next unless line =~ /\s$/
source.error self, index + 1,
"Trailing whitespace detected"
end
end
end end
end end

View file

@ -1,47 +1,50 @@
# A rule that disallows the use of an `else` block with the `unless`. module Ameba::Rules
# # 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:
# ``` #
# unless something # ```
# :ok # unless something
# end # :ok
# # end
# if something #
# :one # if something
# else # :one
# :two # else
# end # :two
# ``` # end
# # ```
# But it considers this one invalid as it is an `unless` with an `else`: #
# # But it considers this one invalid as it is an `unless` with an `else`:
# ``` #
# unless something # ```
# :one # unless something
# else # :one
# :two # else
# end # :two
# ``` # end
# # ```
# The solution is to swap the order of the blocks, and change the `unless` to #
# an `if`, so the previous invalid example would become this: # The solution is to swap the order of the blocks, and change the `unless` to
# # an `if`, so the previous invalid example would become this:
# ``` #
# if something # ```
# :two # if something
# else # :two
# :one # else
# end # :one
# ``` # end
# ```
struct UnlessElse < Rule
def test(source)
UnlessVisitor.new self, source
end
Ameba.rule UnlessElse do |source| def test(source, node : Crystal::Unless)
UnlessElseVisitor.new self, source unless node.else.is_a?(Crystal::Nop)
end source.error self, node.location.try &.line_number,
"Favour if over unless with else"
Ameba.visitor UnlessElse, Crystal::Unless do |node| end
unless node.else.is_a?(Crystal::Nop) end
@source.error @rule, node.location.try &.line_number,
"Favour if over unless with else"
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