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
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Ameba.visitor ComparisonToBoolean, Crystal::Call do |node|
 | 
					 | 
				
			||||||
      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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Ameba.visitor UnlessElse, Crystal::Unless do |node|
 | 
					 | 
				
			||||||
      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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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