# frozen_string_literal: true # # Copyright (C) 2016 - present Instructure, Inc. # # This file is part of Canvas. # # Canvas is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, version 3 of the License. # # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # $LOAD_PATH.push File.expand_path("../gems/dr_diff/lib", __dir__) require "dr_diff" class Linter DEFAULT_OPTIONS = { append_files_to_command: false, auto_correct: false, boyscout_mode: true, campsite_mode: true, command: nil, comment_post_processing: proc { |comments| comments }, custom_comment_generation: false, env_sha: ENV["SHA"] || ENV["GERRIT_PATCHSET_REVISION"], format: nil, file_regex: /./, generate_comment_proc: proc {}, gerrit_patchset: !!ENV["GERRIT_PATCHSET_REVISION"], heavy_mode: false, include_git_dir_in_output: !!!ENV["GERRIT_PATCHSET_REVISION"], linter_name: nil, plugin: ENV["GERRIT_PROJECT"], skip_file_size_check: false, skip_wips: false, base_dir: nil, severe_anywhere: true }.freeze def initialize(options = {}) options = DEFAULT_OPTIONS.merge(options) if options[:plugin] == "canvas-lms" options[:plugin] = nil end if options[:plugin].nil? canvas_dir = File.expand_path("..", __dir__) plugins_dir = File.join(canvas_dir, "gems/plugins") if Dir.pwd.start_with?(plugins_dir) options[:plugin] = Dir.pwd[(plugins_dir.length + 1)..] Dir.chdir(canvas_dir) end end options.each do |key, value| instance_variable_set(:"@#{key}", value) end end def run if git_dir && !Dir.exist?(git_dir) puts "No plugin #{plugin} found" return false end if skip_wips && wip? puts "WIP detected, #{linter_name} will not run." return true end if !skip_file_size_check && files.empty? puts "No #{file_regex} file changes found, skipping #{linter_name} check!" return true end publish_comments end def severe_levels return @severe_levels if @severe_levels boyscout_mode ? %w[info warn error fatal] : %w[warn error fatal] end private attr_reader(*DEFAULT_OPTIONS.keys) def git_dir @git_dir ||= plugin && "gems/plugins/#{plugin}/" end def dr_diff @dr_diff ||= ::DrDiff::Manager.new(git_dir:, sha: env_sha, campsite: campsite_mode, heavy: heavy_mode, base_dir:, severe_anywhere:) end def wip? dr_diff.wip? end def changes dr_diff.changes end def files @files ||= dr_diff.files(file_regex) end def full_command if append_files_to_command "#{command} #{files.join(" ")}" else command end end def comments @comments ||= dr_diff.comments(format:, command: full_command, include_git_dir_in_output:, severe_levels:) end def generate_comments if custom_comment_generation generate_comment_proc.call(changes:, auto_correct:, diff: dr_diff&.parsed_diff) else comments end end def publish_comments processed_comments = comment_post_processing.call(generate_comments) if processed_comments.empty? puts "-- -- -- -- -- -- -- -- -- -- --" puts "No relevant #{linter_name} errors found!" puts "-- -- -- -- -- -- -- -- -- -- --" return true end if gerrit_patchset if boyscout_mode processed_comments.each do |comment| comment[:severity] = "error" unless comment[:ignore_boyscout_severity_change] end end publish_gergich_comments(processed_comments) else publish_local_comments(processed_comments) if auto_correct puts "Errors detected and possibly auto corrected." puts "Fix and/or git add the corrections and try to commit again." end end boyscout_mode ? false : processed_comments.any? { |c| severe_levels.include?(c[:severity]) } end def publish_gergich_comments(comments) require "gergich" draft = Gergich::Draft.new cover_comments, comments = comments.partition do |comment| comment[:cover_message] end comments.each do |comment| message = "[#{comment[:source]}] " message << "#{comment[:rule]}: " if comment[:rule] message << comment[:message] draft.add_comment comment[:path], comment[:position], message, comment[:severity] end cover_comments.each do |cover_comment| draft.add_message(cover_comment[:message]) end end def publish_local_comments(comments) require "colorize" comments.each { |comment| pretty_comment(comment) } end def pretty_comment(comment) message = +"" severity_color = severe_levels.include?(comment[:severity]) ? :red : :yellow severity_color = :green if comment[:corrected] message << "[#{comment[:severity]}]".colorize(severity_color) unless comment[:cover_message] message << " #{comment[:path].colorize(:light_blue)}:#{comment[:position]}" end message << " => " message << "[Correctable] ".colorize(:yellow) if comment[:correctable] message << "[Corrected] ".colorize(:green) if comment[:corrected] message << "#{comment[:rule]}: " if comment[:rule] message << comment[:message].colorize(:green) puts message end end