# frozen_string_literal: true module ThemeCheck class Offense include PositionHelper MAX_SOURCE_EXCERPT_SIZE = 120 attr_reader :check, :message, :theme_file, :node, :markup, :line_number, :correction def initialize( check:, # instance of a ThemeCheck::Check message: nil, # error message for the offense theme_file: nil, # ThemeFile node: nil, # Node markup: nil, # string line_number: nil, # line number of the error (1-indexed) # node_markup_offset is the index inside node.markup to start # looking for markup :mindblow:. # This is so we can accurately highlight node substrings. # e.g. if we have the following scenario in which we # want to highlight the middle comma # * node.markup == "replace ',',', '" # * markup == "," # Then we need some way of telling our Position class to start # looking for the second comma. This is done with node_markup_offset. # More context can be found in #376. node_markup_offset: 0, correction: nil # block ) @check = check @correction = correction if message @message = message elsif defined?(check.class::MESSAGE) @message = check.class::MESSAGE else raise ArgumentError, "message required" end @node = node @theme_file = node&.theme_file || theme_file @markup = markup || node&.markup raise ArgumentError, "Offense markup cannot be an empty string" if @markup.is_a?(String) && @markup.empty? @line_number = line_number || @node&.line_number @position = Position.new( @markup, @theme_file&.source, line_number_1_indexed: @line_number, node_markup_offset: node_markup_offset, node_markup: node&.markup ) end def source_excerpt return unless line_number @source_excerpt ||= begin excerpt = theme_file.source_excerpt(line_number) if excerpt.size > MAX_SOURCE_EXCERPT_SIZE excerpt[0, MAX_SOURCE_EXCERPT_SIZE - 3] + '...' else excerpt end end end def in_range?(other_range) # Zero length ranges are OK and considered the same as size 1 ranges other_range = other_range.first..other_range.end if other_range.size == 0 # rubocop:disable Style/ZeroLengthPredicate range.cover?(other_range) || other_range.cover?(range) end def range @range ||= if start_index == end_index (start_index..end_index) else (start_index...end_index) # end_index is excluded end end def start_index @position.start_index end def start_row @position.start_row end def start_column @position.start_column end def end_index @position.end_index end def end_row @position.end_row end def end_column @position.end_column end def code_name check.code_name end def markup_start_in_excerpt source_excerpt.index(markup) if markup end def severity check.severity end def check_name StringHelpers.demodulize(check.class.name) end def version theme_file&.version end def doc check.doc end def location tokens = [theme_file&.relative_path, line_number].compact tokens.join(":") if tokens.any? end def location_range tokens = [theme_file&.relative_path, start_index, end_index].compact tokens.join(":") if tokens.any? end def correctable? !!correction end def correct(corrector = nil) if correctable? corrector ||= Corrector.new(theme_file: theme_file) correction.call(corrector) end rescue => e ThemeCheck.bug(<<~EOS) Exception while running `Offense#correct`: ``` #{e.class}: #{e.message} #{e.backtrace.join("\n ")} ``` Offense: ``` #{JSON.pretty_generate(to_h)} ``` Check options: ``` #{check.options.pretty_inspect} ``` Markup: ``` #{markup} ``` Node.Markup: ``` #{node&.markup} ``` EOS exit(2) end def whole_theme? check.whole_theme? end def single_file? check.single_file? end def ==(other) other.is_a?(Offense) && code_name == other.code_name && message == other.message && location == other.location && start_index == other.start_index && end_index == other.end_index end alias_method :eql?, :== def to_s if theme_file "#{message} at #{location}" else message end end def to_s_range if theme_file "#{message} at #{location_range}" else message end end def to_h { check: check.code_name, path: theme_file&.relative_path, severity: check.severity_value, start_row: start_row, start_column: start_column, end_row: end_row, end_column: end_column, message: message, } end end end