Performance improvements (#15)

* Performance improvements

Two main changes:

1. Cache parsed AST in a Source. So Parser will parse content only once.
2. Use one unified visitor with multiple callbacks instead of multiple
visitors to traverse AST.

This improves performance significantly, for example running it on
Crystal repository takes ~1 second, which 6 times faster than before.

* Change readme example
This commit is contained in:
V. Elenhaupt 2017-11-06 20:54:58 +02:00 committed by GitHub
parent f27e32cbea
commit adfe654733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 34 additions and 42 deletions

View file

@ -67,22 +67,21 @@ your logic to detect a problem:
```crystal
struct DebuggerStatement < Rule
# This is a required method to be implemented by the rule.
# Source will pass here. If rule finds an issue in this source,
# it adds an error:
# Source will be passed here. If rule finds an issue in this source,
# it reports an error:
#
# source.error rule, line_number, message
#
def test(source)
# This line deletegates verification to a particular AST visitor.
AST::CallVisitor.new self, source
AST::Visitor.new self, source
end
# This method is called once our visitor finds a required node.
#
# It reports an error, once there is `debugger` method call
# without arguments and a receiver. That's it, somebody forgot
# to remove debugger statement.
# This method is called once the visitor finds a required node.
def test(source, node : Crystal::Call)
# It reports an error, if there is `debugger` method call
# without arguments and a receiver. That's it, somebody forgot
# to remove a debugger statement.
return unless node.name == "debugger" && node.args.empty? && node.obj.nil?
source.error self, node.location.try &.line_number,

View file

@ -8,7 +8,7 @@ module Ameba::AST
{% for name in NODE_VISITORS %}
describe "{{name}}" do
it "allow to visit {{name}} node" do
visitor = {{name}}Visitor.new rule, source
visitor = Visitor.new rule, source
nodes = Crystal::Parser.new("").parse
nodes.accept visitor
end

View file

@ -19,26 +19,23 @@ module Ameba::AST
Var,
]
abstract class Visitor < Crystal::Visitor
class Visitor < Crystal::Visitor
@rule : Rule
@source : Source
def initialize(@rule, @source)
parser = Crystal::Parser.new(@source.content)
parser.filename = @source.path
parser.parse.accept self
@source.ast.accept self
end
def visit(node : Crystal::ASTNode)
true
end
end
{% for name in NODE_VISITORS %}
class {{name}}Visitor < Visitor
{% for name in NODE_VISITORS %}
def visit(node : Crystal::{{name}})
@rule.test @source, node
true
end
end
{% end %}
{% end %}
end
end

View file

@ -3,7 +3,7 @@ module Ameba
abstract def test(source : Source)
def test(source : Source, node : Crystal::ASTNode)
raise "Unimplemented"
# can't be abstract
end
def catch(source : Source)

View file

@ -14,7 +14,7 @@ module Ameba::Rules
#
struct ComparisonToBoolean < Rule
def test(source)
AST::CallVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Call)

View file

@ -17,7 +17,7 @@ module Ameba::Rules
#
struct ConstantNames < Rule
def test(source)
AST::AssignVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Assign)

View file

@ -6,7 +6,7 @@ module Ameba::Rules
#
struct DebuggerStatement < Rule
def test(source)
AST::CallVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Call)

View file

@ -17,9 +17,7 @@ module Ameba::Rules
include AST::Util
def test(source)
AST::IfVisitor.new self, source
AST::UnlessVisitor.new self, source
AST::CaseVisitor.new self, source
AST::Visitor.new self, source
end
def check_node(source, node)

View file

@ -13,7 +13,7 @@ module Ameba::Rules
include AST::Util
def test(source)
AST::StringInterpolationVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::StringInterpolation)

View file

@ -32,7 +32,7 @@ module Ameba::Rules
# ```
struct MethodNames < Rule
def test(source)
AST::DefVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Def)

View file

@ -22,7 +22,7 @@ module Ameba::Rules
#
struct NegatedConditionsInUnless < Rule
def test(source)
AST::UnlessVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Unless)

View file

@ -24,7 +24,7 @@ module Ameba::Rules
#
struct PredicateName < Rule
def test(source)
AST::DefVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Def)

View file

@ -47,13 +47,7 @@ module Ameba::Rules
#
struct TypeNames < Rule
def test(source)
[
AST::ClassDefVisitor,
AST::EnumDefVisitor,
AST::ModuleDefVisitor,
AST::AliasVisitor,
AST::LibDefVisitor,
].each(&.new self, source)
AST::Visitor.new self, source
end
private def check_node(source, node)

View file

@ -38,7 +38,7 @@ module Ameba::Rules
#
struct UnlessElse < Rule
def test(source)
AST::UnlessVisitor.new self, source
AST::Visitor.new self, source
end
def test(source, node : Crystal::Unless)

View file

@ -26,11 +26,7 @@ module Ameba::Rules
#
struct VariableNames < Rule
def test(source)
[
AST::VarVisitor,
AST::InstanceVarVisitor,
AST::ClassVarVisitor,
].each &.new(self, source)
AST::Visitor.new self, source
end
private def check_node(source, node)

View file

@ -15,6 +15,7 @@ module Ameba
getter errors = [] of Error
getter path : String?
getter content : String
getter ast : Crystal::ASTNode?
def initialize(@content : String, @path = nil)
end
@ -30,5 +31,12 @@ module Ameba
def lines
@lines ||= @content.split("\n")
end
def ast
@ast ||=
Crystal::Parser.new(content)
.tap { |parser| parser.filename = @path }
.parse
end
end
end