-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add lib/manageiq/style/linter classes
- Loading branch information
1 parent
bd84cc5
commit 9733bab
Showing
5 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.