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_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 172

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

#cop_config(file, offense) (private)

[ GitHub ]

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

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 159

def default_level_for(file, offense, cop_class)
  severity = cop_config(file, offense)['Severity'] ||
             (cop_class&.lint? ? :warning : :convention)

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

#description_for(file, offense) (private)

[ GitHub ]

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

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 60

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)
  offenses.each do |offense|
    @results << result_for(file, offense)
  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) (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.

[ GitHub ]

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

def fingerprints_for(offense)
  source_line = offense.location.source_line
  { primaryLocationLineHash: Digest::SHA256.hexdigest("#{offense.cop_name}#{source_line}") }
end

#finished(_inspected_files)

[ GitHub ]

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

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 149

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 91

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 100

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 52

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

#result_for(file, offense) (private)

[ GitHub ]

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

def result_for(file, offense)
  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)
  }

  # 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 131

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 124

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 118

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 176

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