module Ameba # A module that utilizes inline comments parsing and processing logic. module InlineComments COMMENT_DIRECTIVE_REGEX = /# ameba:(?\w+) (?\w+(?:\/\w+)?(?:,? \w+(?:\/\w+)?)*)/ # Available actions in the inline comments enum Action Disable Enable end # Returns `true` if current location is disabled for a particular rule, # `false` otherwise. # # Location is disabled in two cases: # 1. The line of the location ends with a comment directive. # 2. The line above the location is a comment directive. # # For example, here are two examples of disabled location: # # ``` # # ameba:disable Style/LargeNumbers # Time.epoch(1483859302) # # Time.epoch(1483859302) # ameba:disable Style/LargeNumbers # ``` # # But here are examples which are not considered as disabled location: # # ``` # # ameba:disable Style/LargeNumbers # # # Time.epoch(1483859302) # # if use_epoch? # ameba:disable Style/LargeNumbers # Time.epoch(1483859302) # end # ``` def location_disabled?(location : Crystal::Location?, rule) return false if rule.name.in?(Rule::SPECIAL) return false unless line_number = location.try &.line_number.try &.- 1 return false unless line = lines[line_number]? line_disabled?(line, rule) || (line_number > 0 && (prev_line = lines[line_number - 1]) && comment?(prev_line) && line_disabled?(prev_line, rule)) end # Parses inline comment directive. Returns a tuple that consists of # an action and parsed rules if directive found, nil otherwise. # # ``` # line = "# ameba:disable Rule1, Rule2" # directive = parse_inline_directive(line) # directive[:action] # => "disable" # directive[:rules] # => ["Rule1", "Rule2"] # ``` # # It ignores the directive if it is commented out. # # ``` # line = "# # ameba:disable Rule1, Rule2" # parse_inline_directive(line) # => nil # ``` def parse_inline_directive(line) return unless directive = COMMENT_DIRECTIVE_REGEX.match(line) return if commented_out?(line.gsub(directive[0], "")) { action: directive["action"], rules: directive["rules"].split(/[\s,]/, remove_empty: true), } end # Returns `true` if the line at the given `line_number` is a comment. def comment?(line_number : Int32) return unless line = lines[line_number]? comment?(line) end private def comment?(line : String) line.lstrip.starts_with? '#' end private def line_disabled?(line, rule) return false unless directive = parse_inline_directive(line) return false unless Action.parse?(directive[:action]).try(&.disable?) rules = directive[:rules] rules.includes?(rule.name) || rules.includes?(rule.group) end private def commented_out?(line) commented = false lexer = Crystal::Lexer.new(line).tap(&.comments_enabled = true) Tokenizer.new(lexer).run do |token| commented = true if token.type.comment? end commented end end end