123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Formatter::SARIFFormatter

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: RuboCop::Formatter::BaseFormatter
Defined in: lib/rubocop/formatter/sarif_formatter.rb

Overview

This formatter formats the report data in SARIF 2.1.0, the OASIS standard for static analysis results. It’s understood by GitHub code scanning, Azure DevOps, and other tools that aggregate analyzer output.

SARIF only knows three severity levels, so RuboCop’s info, refactor and convention severities are all reported as note.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

BaseFormatter - Inherited

#file_finished

Invoked at the end of inspecting each files.

#file_problems

Invoked before #file_finished for a file that a cop crashed on or raised a warning for, or whose corrections went into an infinite loop.

#file_started

Invoked at the beginning of inspecting each files.

#finished

Invoked after all files are inspected or interrupted by user.

#started

Invoked once before any files are inspected.

Constructor Details

.new(output, options = {}) ⇒ SARIFFormatter

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 27

def initialize(output, options = {})
  super

  @rules = []
  @rule_indexes = {}
  @results = []
  @config_store = nil
end

Instance Method Details

#config_for(file) (private)

#file_started is not called when a formatter is used directly through the API, so there may be no config to consult.

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 177

def config_for(file)
  @config_store&.for_file(file)
end

#cop_config(file, offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 171

def cop_config(file, offense)
  config_for(file)&.for_cop(offense.cop_name) || {}
end

#default_level_for(file, offense, cop_class) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 164

def default_level_for(file, offense, cop_class)
  severity = cop_config(file, offense)['Severity'] ||
             Cop::Base::DEPARTMENT_SEVERITIES.fetch(cop_class&.department, :convention)

  SEVERITY_LEVELS.fetch(severity.to_sym, 'warning')
end

#description_for(file, offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 160

def description_for(file, offense)
  cop_config(file, offense)['Description'] || offense.cop_name
end

#driver (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 61

def driver
  {
    name: 'RuboCop',
    version: RuboCop::Version::STRING,
    semanticVersion: RuboCop::Version::STRING,
    informationUri: INFORMATION_URI,
    rules: @rules
  }
end

#file_finished(file, offenses)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 40

def file_finished(file, offenses)
  line_hash_counts = Hash.new(0)
  offenses.each do |offense|
    @results << result_for(file, offense, line_hash_counts)
  end
end

#file_started(_file, options)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 36

def file_started(_file, options)
  @config_store = options[:config_store]
end

#fingerprints_for(offense, line_hash_counts) (private)

GitHub uses these to match an alert across runs. Without them it falls back to the file path, which makes alerts churn whenever a file moves. Repeats of the same hash within a file get a :N suffix, like GitHub’s own fingerprints, so two offenses on one line don’t merge into one alert. The first occurrence keeps the bare hash to match earlier reports.

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 117

def fingerprints_for(offense, line_hash_counts)
  line_hash = Digest::SHA256.hexdigest("#{offense.cop_name}#{offense.location.source_line}")
  count = line_hash_counts[line_hash] += 1
  { primaryLocationLineHash: count == 1 ? line_hash : "#{line_hash}:#{count}" }
end

#finished(_inspected_files)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 47

def finished(_inspected_files)
  output.write(report.to_json)
end

#help_for(description, help_uri) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 154

def help_for(description, help_uri)
  help = { text: description }
  help[:markdown] = "#{description}\n\n[Documentation](#{help_uri})" if help_uri
  help
end

#location_for(file, offense) (private)

The URI is relative to the working directory, which is what consumers expect to resolve against their own checkout. No uriBaseId: it would have to be declared under originalUriBaseIds to mean anything, and every consumer that matters resolves relative paths without it.

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 92

def location_for(file, offense)
  {
    physicalLocation: {
      artifactLocation: { uri: uri_for(file) },
      region: region_for(offense)
    }
  }
end

#region_for(offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 101

def region_for(offense)
  # SARIF columns are 1-based and `endColumn` points at the character
  # *after* the region, while `real_last_column` is the last character.
  {
    startLine: offense.first_line,
    startColumn: offense.real_column,
    endLine: offense.last_line,
    endColumn: offense.real_last_column + 1
  }
end

#report (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 53

def report
  {
    '$schema': SCHEMA_URI,
    version: SARIF_VERSION,
    runs: [{ tool: { driver: driver }, results: @results }]
  }
end

#result_for(file, offense, line_hash_counts) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 71

def result_for(file, offense, line_hash_counts)
  result = {
    ruleId: offense.cop_name,
    ruleIndex: rule_index_for(file, offense),
    level: SEVERITY_LEVELS.fetch(offense.severity.name, 'warning'),
    message: { text: offense.message },
    locations: [location_for(file, offense)],
    partialFingerprints: fingerprints_for(offense, line_hash_counts)
  }

  # Suppressed offenses are only reported under `--display-suppressed`.
  # SARIF has a first-class representation for them, so consumers can
  # tell them apart from offenses that were never triggered.
  result[:suppressions] = [suppression_for(offense)] if offense.disabled?
  result
end

#rule_for(file, offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 136

def rule_for(file, offense)
  cop_class = Cop::Registry.global.find_by_cop_name(offense.cop_name)
  description = description_for(file, offense)
  help_uri = cop_class && Cop::Documentation.url_for(cop_class, config_for(file))

  rule = {
    id: offense.cop_name,
    name: offense.cop_name,
    shortDescription: { text: description },
    fullDescription: { text: description },
    help: help_for(description, help_uri),
    defaultConfiguration: { level: default_level_for(file, offense, cop_class) },
    properties: { tags: [offense.cop_name.split('/').first] }
  }
  rule[:helpUri] = help_uri if help_uri
  rule
end

#rule_index_for(file, offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 129

def rule_index_for(file, offense)
  @rule_indexes[offense.cop_name] ||= begin
    @rules << rule_for(file, offense)
    @rules.size - 1
  end
end

#suppression_for(offense) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 123

def suppression_for(offense)
  suppression = { kind: 'inSource' }
  suppression[:justification] = offense.justification if offense.justification
  suppression
end

#uri_for(file) (private)

[ GitHub ]

  
# File 'lib/rubocop/formatter/sarif_formatter.rb', line 181

def uri_for(file)
  PathUtil.smart_path(file).tr('\\', '/')
end