Skip to content

Commit

Permalink
Add lib/manageiq/style/linter classes
Browse files Browse the repository at this point in the history
  • Loading branch information
NickLaMuro committed Feb 3, 2021
1 parent bd84cc5 commit 9733bab
Show file tree
Hide file tree
Showing 5 changed files with 405 additions and 0 deletions.
180 changes: 180 additions & 0 deletions lib/manageiq/style/linter/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
module ManageIQ
module Style
module Linter
class Base
attr_reader :branch_options, :files_from_cli, :logger, :options

OUTPUT_FORMAT = "%<path>s:%<line>d:%<column>d: %<message>s\n".freeze
DEFAULT_BRANCH_OPTIONS = {
:pull_request => true,
:merge_target => "master",
}.freeze


def initialize(files_from_cli, options = {})
require 'logger'

branch_option_args = options.delete(:branch_options) { |_| {} }
@branch_options = DEFAULT_BRANCH_OPTIONS.dup.merge(branch_option_args)
@branch_options[:repo_path] ||= Dir.pwd
@branch_options[:repo_name] ||= File.basename(@branch_options[:repo_path])

@options = options
@options[:output] = :stdout # :stdout or :logger
@options[:logger_io] = STDOUT if @options[:debug]

@files_from_cli = files_from_cli || []

set_logger
end

def run
logger.info("#{log_header} Starting run...")
if files_to_lint.empty?
logger.info("#{log_header} Skipping run due to no candidate files.")
return
end

require 'tempfile'
result = Dir.mktmpdir do |dir|
files = collected_config_files(dir)
if files.blank?
logger.error("#{log_header} Failed to run due to missing config files.")
return failed_linter_offenses("missing config files")
else
files += collected_files_to_lint(dir)
logger.info("#{log_header} Collected #{files.length} files.")
logger.debug { "#{log_header} File list: #{files.inspect}"}
run_linter(dir)
end
end

offenses = parse_output(result.output)
logger.info("#{log_header} Completed run with #{offenses.fetch_path('summary', 'offense_count')} offenses")
output_offenses offenses
offenses
end

private

def output_offenses(offenses_json)
case options[:output]
when :stdout
offenses_json["files"].each do |file|
file["offenses"].each do |offense|
message = "#{offense["severity"]}: #{offense["cop_name"]}: #{offense["message"]}"
STDOUT.printf(OUTPUT_FORMAT, :path => file["path"],
:line => offense["location"]["line"],
:column => offense["location"]["column"],
:message => message)
end
end
when :logger
logger.debug { "#{log_header} Offenses: #{offenses.inspect}" }
end
end

def parse_output(output)
JSON.parse(output.chomp)
rescue JSON::ParserError => error
logger.error("#{log_header} #{error.message}")
logger.error("#{log_header} Failed to parse JSON result #{output.inspect}")
return failed_linter_offenses("error parsing JSON result")
end

def collected_config_files(dir)
config_files.select { |path| extract_file(path, dir, branch_service.pull_request?) }
end

def collected_files_to_lint(dir)
files_to_lint.select { |path| extract_file(path, dir) }
end

def extract_file(path, destination_dir, merged = false)
content = branch_service.content_at(path, merged)
return false unless content

perm = branch_service.permission_for(path, merged)
temp_file = File.join(destination_dir, path)
FileUtils.mkdir_p(File.dirname(temp_file))

# Use "wb" to prevent Encoding::UndefinedConversionError: "\xD0" from
# ASCII-8BIT to UTF-8
File.write(temp_file, content, :mode => "wb", :perm => perm)

true
end

def branch_service
@branch_service ||= GitService::Branch.new(branch_options)
end

def diff_service
@diff_service ||= branch_service.diff
end

def files_to_lint
@files_to_lint ||= begin
unfiltered_files = branch_service.pull_request? ? diff_service.new_files : branch_service.tip_files
unfiltered_files &= files_from_cli unless files_from_cli.empty?
# puts "unfiltered_files & files_from_cli = #{unfiltered_files & files_from_cli}"
# puts "files_from_cli & unfiltered_files = #{files_from_cli & unfiltered_files}"
filtered_files(unfiltered_files)
end
end

def run_linter(dir)
logger.info("#{log_header} Executing linter...")
require 'awesome_spawn'
result = AwesomeSpawn.run(linter_executable, :params => linter_cli_options, :chdir => dir)
handle_linter_output(result)
end

def handle_linter_output(result)
# rubocop exits 1 both when there are errors and when there are style issues.
# Instead of relying on just exit_status, we check if there is anything on stderr.
return result if result.exit_status.zero? || result.error.blank?
FailedLinterRun.new(failed_linter_offenses("#{self.class.name} STDERR:\n```\n#{result.error}\n```"))
end

def failed_linter_offenses(message)
{
"files" => [
{
"path" => "\\*\\*",
"offenses" => [
{
"severity" => "fatal",
"message" => message,
"cop_name" => self.class.name.titleize
}
]
}
],
"summary" => {
"offense_count" => 1,
"target_file_count" => files_to_lint.length,
"inspected_file_count" => files_to_lint.length
}
}
end

def log_header
"#{self.class.name} Repo: #{@branch_options[:repo_name]} Branch #{branch_service.name} -"
end

class FailedLinterRun
attr_reader :output
def initialize(message)
@output = message.to_json
end
end

def set_logger
logger_io = @options[:logfile] || File::NULL
@logger = Logger.new(logger_io)
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/manageiq/style/linter/haml.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'manageiq/style/linter/base'

module ManageIQ
module Style
module Linter
class Haml < Base
private

def config_files
[".haml-lint.yml"] + Linter::Rubocop::CONFIG_FILES
end

def linter_executable
'haml-lint *'
end

def linter_cli_options
{:reporter => 'json'}
end

def filtered_files(files)
files.select { |file| file.end_with?(".haml") }
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/manageiq/style/linter/rubocop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'manageiq/style/linter/base'

require 'rubocop'

module ManageIQ
module Style
module Linter
class Rubocop < Base
CONFIG_FILES = %w[.rubocop.yml .rubocop_base.yml .rubocop_local.yml].freeze

private

def config_files
CONFIG_FILES
end

def linter_executable
'rubocop'
end

def linter_cli_options
{:format => 'json', :no_display_cop_names => nil}
end

def filtered_files(files)
files.select do |file|
file.end_with?(".rb", ".ru", ".rake") || %w[Gemfile Rakefile].include?(File.basename(file))
end.reject do |file|
file.end_with?("db/schema.rb")
end
end
end
end
end
end
65 changes: 65 additions & 0 deletions lib/manageiq/style/linter/yaml.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'manageiq/style/linter/base'
require 'more_core_extensions/core_ext/hash'

module ManageIQ
module Style
module Linter
class Yaml < Base
private

def parse_output(output)
lines = output.chomp.split("\n")
parsed = lines.collect { |line| line_to_hash(line) }
grouped = parsed.group_by { |hash| hash["filename"] }
file_count = parsed.collect { |hash| hash["filename"] }.uniq.count
{
"files" => grouped.collect do |filename, offenses|
{
"path" => filename.sub(%r{\A\./}, ""),
"offenses" => offenses.collect { |offense_hash| offense_hash.deep_delete("filename"); }
}
end,
"summary" => {
"offense_count" => lines.size,
"target_file_count" => file_count,
"inspected_file_count" => file_count
}
}
end

def linter_executable
"yamllint"
end

def config_files
[".yamllint"]
end

def linter_cli_options
{:f => "parsable", nil => ["."]}
end

def filtered_files(files)
files.select { |f| f.end_with?(".yml", ".yaml") }
end

def line_to_hash(line)
filename, line, column, severity_message_cop = line.split(":", 4)
severity_message, cop = severity_message_cop.split(/ \((.*)\)\Z/)
severity, message = severity_message.match(/\[(.*)\] (.*)/).captures

{
"filename" => filename,
"severity" => severity,
"message" => message,
"cop_name" => cop,
"location" => {
"line" => line.to_i,
"column" => column.to_i
}
}
end
end
end
end
end
Loading

0 comments on commit 9733bab

Please sign in to comment.