mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
parent
6cef83f9a9
commit
f1e462cc86
9 changed files with 142 additions and 122 deletions
27
src/ameba/ast.cr
Normal file
27
src/ameba/ast.cr
Normal 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
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue