# frozen_string_literal: true require 'json' module Danger class DangerKtlint < Plugin class UnexpectedLimitTypeError < StandardError; end class UnsupportedServiceError < StandardError def initialize(message = 'Unsupported service! Currently supported services are GitHub, GitLab and BitBucket server.') super(message) end end AVAILABLE_SERVICES = [:github, :gitlab, :bitbucket_server] # TODO: Lint all files if `filtering: false` attr_accessor :filtering attr_accessor :skip_lint, :report_file, :report_files_pattern def limit @limit ||= nil end def limit=(limit) if limit != nil && limit.integer? @limit = limit else raise UnexpectedLimitTypeError end end # Run ktlint task using command line interface # Will fail if `ktlint` is not installed # Skip lint task if files changed are empty # @return [void] # def lint(inline_mode: false) def lint(inline_mode: false) unless supported_service? raise UnsupportedServiceError.new end targets = target_files(git.added_files + git.modified_files) results = ktlint_results(targets) if results.nil? || results.empty? return end if inline_mode send_inline_comments(results, targets) else send_markdown_comment(results, targets) end end # Comment to a PR by ktlint result json # # // Sample single ktlint result # [ # { # "file": "app/src/main/java/com/mataku/Model.kt", # "errors": [ # { # "line": 46, # "column": 1, # "message": "Unexpected blank line(s) before \"}\"", # "rule": "no-blank-line-before-rbrace" # } # ] # } # ] def send_markdown_comment(ktlint_results, targets) catch(:loop_break) do count = 0 ktlint_results.each do |ktlint_result| ktlint_result.each do |result| result['errors'].each do |error| file_path = relative_file_path(result['file']) next unless targets.include?(file_path) message = "#{file_html_link(file_path, error['line'])}: #{error['message']}" fail(message) unless limit.nil? count += 1 if count >= limit throw(:loop_break) end end end end end end end def send_inline_comments(ktlint_results, targets) catch(:loop_break) do count = 0 ktlint_results.each do |ktlint_result| ktlint_result.each do |result| result['errors'].each do |error| file_path = relative_file_path(result['file']) next unless targets.include?(file_path) message = error['message'] line = error['line'] fail(message, file: result['file'], line: line) unless limit.nil? count += 1 if count >= limit throw(:loop_break) end end end end end end end def target_files(changed_files) changed_files.select do |file| file.end_with?('.kt') end end # Make it a relative path so it can compare it to git.added_files def relative_file_path(file_path) file_path.gsub(/#{pwd}\//, '') end private def file_html_link(file_path, line_number) file = if danger.scm_provider == :github "#{file_path}#L#{line_number}" else file_path end scm_provider_klass.html_link(file) end # `eval` may be dangerous, but it does not accept any input because it accepts only defined as danger.scm_provider def scm_provider_klass @scm_provider_klass ||= eval(danger.scm_provider.to_s) end def pwd @pwd ||= `pwd`.chomp end def ktlint_exists? system 'which ktlint > /dev/null 2>&1' end def ktlint_results(targets) if skip_lint # TODO: Allow XML ktlint_result_files.map do |file| File.open(file) do |f| JSON.load(f) end end else unless ktlint_exists? fail("Couldn't find ktlint command. Install first.") return end return if targets.empty? [JSON.parse(`ktlint #{targets.join(' ')} --reporter=json --relative`)] end end def supported_service? AVAILABLE_SERVICES.include?(danger.scm_provider.to_sym) end def ktlint_result_files if !report_file.nil? && !report_file.empty? && File.exists?(report_file) [report_file] elsif !report_files_pattern.nil? && !report_files_pattern.empty? Dir.glob(report_files_pattern) else fail("Couldn't find ktlint result json file.\nYou must specify it with `ktlint.report_file=...` or `ktlint.report_files_pattern=...` in your Dangerfile.") end end end end