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 def test(source : Source)
def test(source : Source, node : Crystal::ASTNode)
raise "Unimplemented"
end
def catch(source : Source)
source.tap { |s| test s }
end

View file

@ -1,26 +1,29 @@
# A rule that disallows comparison to booleans.
#
# For example, these are considered invalid:
#
# ```
# foo == true
# 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
# the variable.
module Ameba::Rules
# A rule that disallows comparison to booleans.
#
# For example, these are considered invalid:
#
# ```
# foo == true
# 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
# the variable.
struct ComparisonToBoolean < Rule
def test(source)
CallVisitor.new self, source
end
Ameba.rule ComparisonToBoolean do |source|
ComparisonToBooleanVisitor.new self, source
end
Ameba.visitor ComparisonToBoolean, Crystal::Call do |node|
def test(source, node : Crystal::Call)
if %w(== != ===).includes?(node.name) && (
node.args.first?.try &.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"
end
end
end
end

View file

@ -1,8 +1,12 @@
# A rule that disallows lines longer than 79 symbols.
Ameba.rule LineLength do |source|
module Ameba::Rules
# A rule that disallows lines longer than 79 symbols.
struct LineLength < Rule
def test(source)
source.lines.each_with_index do |line, index|
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

View file

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

View file

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

View file

@ -1,47 +1,50 @@
# A rule that disallows the use of an `else` block with the `unless`.
#
# For example, the rule considers these valid:
#
# ```
# unless something
# :ok
# end
#
# if something
# :one
# else
# :two
# end
# ```
#
# But it considers this one invalid as it is an `unless` with an `else`:
#
# ```
# unless something
# :one
# else
# :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:
#
# ```
# if something
# :two
# else
# :one
# end
# ```
module Ameba::Rules
# A rule that disallows the use of an `else` block with the `unless`.
#
# For example, the rule considers these valid:
#
# ```
# unless something
# :ok
# end
#
# if something
# :one
# else
# :two
# end
# ```
#
# But it considers this one invalid as it is an `unless` with an `else`:
#
# ```
# unless something
# :one
# else
# :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:
#
# ```
# if something
# :two
# else
# :one
# end
# ```
struct UnlessElse < Rule
def test(source)
UnlessVisitor.new self, source
end
Ameba.rule UnlessElse do |source|
UnlessElseVisitor.new self, source
end
Ameba.visitor UnlessElse, Crystal::Unless do |node|
def test(source, node : Crystal::Unless)
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"
end
end
end
end

View file

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