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 def test(source : Source)
 | 
			
		||||
 | 
			
		||||
    def test(source : Source, node : Crystal::ASTNode)
 | 
			
		||||
      raise "Unimplemented"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def catch(source : Source)
 | 
			
		||||
      source.tap { |s| test s }
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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|
 | 
			
		||||
  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,
 | 
			
		||||
      "Comparison to a boolean is pointless"
 | 
			
		||||
    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 self, 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.
 | 
			
		||||
 | 
			
		||||
Ameba.rule LineLength do |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)"
 | 
			
		||||
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)"
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,11 @@
 | 
			
		|||
# A rule that disallows trailing blank lines at the end of the source file.
 | 
			
		||||
 | 
			
		||||
Ameba.rule TrailingBlankLines do |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"
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,12 @@
 | 
			
		|||
# A rule that disallows trailing whitespace at the end of a line.
 | 
			
		||||
 | 
			
		||||
Ameba.rule TrailingWhitespace do |source|
 | 
			
		||||
  source.lines.each_with_index do |line, index|
 | 
			
		||||
    next unless line =~ /\s$/
 | 
			
		||||
    source.error self, index + 1, "Trailing whitespace detected"
 | 
			
		||||
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"
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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|
 | 
			
		||||
  unless node.else.is_a?(Crystal::Nop)
 | 
			
		||||
    @source.error @rule, node.location.try &.line_number,
 | 
			
		||||
      "Favour if over unless with else"
 | 
			
		||||
    def test(source, node : Crystal::Unless)
 | 
			
		||||
      unless node.else.is_a?(Crystal::Nop)
 | 
			
		||||
        source.error self, 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
 | 
			
		||||
  # 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue