Class: Danger::DangerSwiftlint

Inherits:
Plugin
  • Object
show all
Defined in:
lib/danger_plugin.rb

Overview

Lint Swift files inside your projects. This is done using the [SwiftLint](github.com/realm/SwiftLint) tool. Results are passed out as a table in markdown.

Examples:

Specifying custom config file.


# Runs a linter with comma style disabled
swiftlint.config_file = '.swiftlint.yml'
swiftlint.lint_files

See Also:

  • artsy/eigen

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#binary_pathObject

The path to SwiftLint’s execution



24
25
26
# File 'lib/danger_plugin.rb', line 24

def binary_path
  @binary_path
end

#config_fileObject

The path to SwiftLint’s configuration file



27
28
29
# File 'lib/danger_plugin.rb', line 27

def config_file
  @config_file
end

#directoryObject

Allows you to specify a directory from where swiftlint will be run.



30
31
32
# File 'lib/danger_plugin.rb', line 30

def directory
  @directory
end

#errorsObject

Errors found



48
49
50
# File 'lib/danger_plugin.rb', line 48

def errors
  @errors
end

#filter_issues_in_diffObject

Whether all issues or ones in PR Diff to be reported



54
55
56
# File 'lib/danger_plugin.rb', line 54

def filter_issues_in_diff
  @filter_issues_in_diff
end

#issuesObject

All issues found



51
52
53
# File 'lib/danger_plugin.rb', line 51

def issues
  @issues
end

#lint_all_filesObject

Whether all files should be linted in one pass



39
40
41
# File 'lib/danger_plugin.rb', line 39

def lint_all_files
  @lint_all_files
end

#max_num_violationsObject

Maximum number of issues to be reported.



33
34
35
# File 'lib/danger_plugin.rb', line 33

def max_num_violations
  @max_num_violations
end

#strictObject

Whether we should fail on warnings



42
43
44
# File 'lib/danger_plugin.rb', line 42

def strict
  @strict
end

#verboseObject

Provides additional logging diagnostic information.



36
37
38
# File 'lib/danger_plugin.rb', line 36

def verbose
  @verbose
end

#warningsObject

Warnings found



45
46
47
# File 'lib/danger_plugin.rb', line 45

def warnings
  @warnings
end

Instance Method Details

#filter_git_diff_issues(issues) ⇒ Array

Filters issues reported against changes in the modified files

Returns:

  • (Array)

    swiftlint issues



333
334
335
336
337
338
# File 'lib/danger_plugin.rb', line 333

def filter_git_diff_issues(issues)
  modified_files_info = git_modified_files_info()
  return issues.select { |i| 
       modified_files_info["#{i['file']}"] != nil && modified_files_info["#{i['file']}"].include?(i['line'].to_i) 
    }
end

#find_swift_files(dir_selected, files = nil, excluded_paths = [], included_paths = []) ⇒ Array

Find swift files from the files glob If files are not provided it will use git modifield and added files

Returns:

  • (Array)

    swift files



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/danger_plugin.rb', line 211

def find_swift_files(dir_selected, files = nil, excluded_paths = [], included_paths = [])
  # Assign files to lint
  if files.nil?
    renamed_files_hash = git.renamed_files.map { |rename| [rename[:before], rename[:after]] }.to_h
    post_rename_modified_files = git.modified_files.map { |modified_file| renamed_files_hash[modified_file] || modified_file }
    files = (post_rename_modified_files - git.deleted_files) + git.added_files
  else
    files = Dir.glob(files)
  end
  # Filter files to lint
  excluded_paths_list = Find.find(*excluded_paths).to_a
  included_paths_list = Find.find(*included_paths).to_a
  files.
    # Ensure only swift files are selected
    select { |file| file.end_with?('.swift') }.
    # Convert to absolute paths
    map { |file| File.expand_path(file) }.
    # Remove dups
    uniq.
    # Ensure only files in the selected directory
    select { |file| file.start_with?(dir_selected) }.
    # Reject files excluded on configuration
    reject { |file| excluded_paths_list.include?(file) }.
    # Accept files included on configuration
    select do |file|
      next true if included_paths.empty?
      included_paths_list.include?(file)
    end
end

#format_paths(paths, filepath) ⇒ Array

Parses the configuration file and return the specified files in path

Returns:

  • (Array)

    list of files specified in path



266
267
268
269
270
271
272
# File 'lib/danger_plugin.rb', line 266

def format_paths(paths, filepath)
  # Extract included paths
  paths
    .map { |path| File.join(File.dirname(filepath), path) }
    .map { |path| File.expand_path(path) }
    .select { |path| File.exist?(path) || Dir.exist?(path) }
end

#git_modified_files_infoArray

Finds modified files and added files, creates array of files with modified line numbers

Returns:

  • (Array)

    Git diff changes for each file



343
344
345
346
347
348
349
350
351
# File 'lib/danger_plugin.rb', line 343

def git_modified_files_info()
    modified_files_info = Hash.new
    updated_files = (git.modified_files - git.deleted_files) + git.added_files
    updated_files.each {|file|
        modified_lines = git_modified_lines(file)
        modified_files_info[File.expand_path(file)] = modified_lines
    }
    modified_files_info
end

#git_modified_lines(file) ⇒ Array

Gets git patch info and finds modified line numbers, excludes removed lines

Returns:

  • (Array)

    Modified line numbers i



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/danger_plugin.rb', line 356

def git_modified_lines(file)
  git_range_info_line_regex = /^@@ .+\+(?<line_number>\d+),/ 
  git_modified_line_regex = /^\+(?!\+|\+)/
  git_removed_line_regex = /^\-(?!\-|\-)/
  file_info = git.diff_for_file(file)
  line_number = 0
  lines = []
  file_info.patch.split("\n").each do |line|
      starting_line_number = 0
      case line
      when git_range_info_line_regex
          starting_line_number = Regexp.last_match[:line_number].to_i
      when git_modified_line_regex
          lines << line_number
      end
      line_number += 1 if line_number > 0 && !git_removed_line_regex.match?(line)
      line_number = starting_line_number if starting_line_number > 0
  end
  lines
end

#lint_files(files = nil, inline_mode: false, fail_on_error: false, additional_swiftlint_args: '', no_comment: false, &select_block) ⇒ void

This method returns an undefined value.

Lints Swift files. Will fail if ‘swiftlint` cannot be installed correctly. Generates a `markdown` list of warnings for the prose in a corpus of .markdown and .md files.

Parameters:

  • files (String) (defaults to: nil)

    A globbed string which should return the files that you want to lint, defaults to nil. if nil, modified and added files from the diff will be used.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/danger_plugin.rb', line 66

def lint_files(files = nil, inline_mode: false, fail_on_error: false, additional_swiftlint_args: '', no_comment: false, &select_block)
  # Fails if swiftlint isn't installed
  raise 'swiftlint is not installed' unless swiftlint.installed?

  config_file_path = config_file
  if config_file_path
    log "Using config file: #{config_file_path}"
  else
    log 'Config file was not specified.'
  end

  dir_selected = directory ? File.expand_path(directory) : Dir.pwd
  log "Swiftlint will be run from #{dir_selected}"

  # Get config
  config = load_config(config_file_path)

  # Extract excluded paths
  excluded_paths = format_paths(config['excluded'] || [], config_file_path)

  log "Swiftlint will exclude the following paths: #{excluded_paths}"

  # Extract included paths
  included_paths = format_paths(config['included'] || [], config_file_path)

  log "Swiftlint includes the following paths: #{included_paths}"

  # Prepare swiftlint options
  options = {
    # Make sure we don't fail when config path has spaces
    config: config_file_path ? Shellwords.escape(config_file_path) : nil,
    reporter: 'json',
    quiet: true,
    pwd: dir_selected,
    force_exclude: true
  }
  log "linting with options: #{options}"

  if lint_all_files
    issues = run_swiftlint(options, additional_swiftlint_args)
  else
    # Extract swift files (ignoring excluded ones)
    files = find_swift_files(dir_selected, files, excluded_paths, included_paths)
    log "Swiftlint will lint the following files: #{files.join(', ')}"

    # Lint each file and collect the results
    issues = run_swiftlint_for_each(files, options, additional_swiftlint_args)
  end

  if filter_issues_in_diff
    # Filter issues related to changes in PR Diff
    issues = filter_git_diff_issues(issues)
  end

  @issues = issues
  other_issues_count = 0
  unless @max_num_violations.nil? || no_comment
    other_issues_count = issues.count - @max_num_violations if issues.count > @max_num_violations
    issues = issues.take(@max_num_violations)
  end
  log "Received from Swiftlint: #{issues}"

  # filter out any unwanted violations with the passed in select_block
  if select_block && !no_comment
    issues = issues.select { |issue| select_block.call(issue) }
  end

  # Filter warnings and errors
  @warnings = issues.select { |issue| issue['severity'] == 'Warning' }
  @errors = issues.select { |issue| issue['severity'] == 'Error' }

  # Early exit so we don't comment
  return if no_comment

  if inline_mode
    # Report with inline comment
    send_inline_comment(warnings, strict ? :fail : :warn)
    send_inline_comment(errors, (fail_on_error || strict) ? :fail : :warn)
    warn other_issues_message(other_issues_count) if other_issues_count > 0
  elsif warnings.count > 0 || errors.count > 0
    # Report if any warning or error
    message = "### SwiftLint found issues\n\n".dup
    message << markdown_issues(warnings, 'Warnings') unless warnings.empty?
    message << markdown_issues(errors, 'Errors') unless errors.empty?
    message << "\n#{other_issues_message(other_issues_count)}" if other_issues_count > 0
    markdown message

    # Fail danger on errors
    should_fail_by_errors = fail_on_error && errors.count > 0
    # Fail danger if any warnings or errors and we are strict
    should_fail_by_strict = strict && (errors.count > 0 || warnings.count > 0)
    if should_fail_by_errors || should_fail_by_strict
      fail 'Failed due to SwiftLint errors'
    end
  end
end

#load_config(filepath) ⇒ Object

Get the configuration file



242
243
244
245
246
247
248
249
250
251
# File 'lib/danger_plugin.rb', line 242

def load_config(filepath)
  return {} if filepath.nil? || !File.exist?(filepath)

  config_file = File.open(filepath).read

  # Replace environment variables
  config_file = parse_environment_variables(config_file)

  YAML.safe_load(config_file)
end

#log(text) ⇒ Object



326
327
328
# File 'lib/danger_plugin.rb', line 326

def log(text)
  puts(text) if @verbose
end

#markdown_issues(results, heading) ⇒ String

Create a markdown table from swiftlint issues

Returns:

  • (String)


277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/danger_plugin.rb', line 277

def markdown_issues(results, heading)
  message = "#### #{heading}\n\n".dup

  message << "File | Line | Reason |\n"
  message << "| --- | ----- | ----- |\n"

  results.each do |r|
    filename = r['file'].split('/').last
    line = r['line']
    reason = r['reason']
    rule = r['rule_id']
    # Other available properties can be found int SwiftLint/…/JSONReporter.swift
    message << "#{filename} | #{line} | #{reason} (#{rule})\n"
  end

  message
end

#other_issues_message(issues_count) ⇒ Object



314
315
316
317
# File 'lib/danger_plugin.rb', line 314

def other_issues_message(issues_count)
  violations = issues_count == 1 ? 'violation' : 'violations'
  "SwiftLint also found #{issues_count} more #{violations} with this PR."
end

#parse_environment_variables(file_contents) ⇒ Object

Find all requested environment variables in the given string and replace them with the correct values.



254
255
256
257
258
259
260
261
# File 'lib/danger_plugin.rb', line 254

def parse_environment_variables(file_contents)
  # Matches the file contents for environment variables defined like ${VAR_NAME}.
  # Replaces them with the environment variable value if it exists.
  file_contents.gsub(/\$\{([^{}]+)\}/) do |env_var|
    return env_var if ENV[Regexp.last_match[1]].nil?
    ENV[Regexp.last_match[1]]
  end
end

#run_swiftlint(options, additional_swiftlint_args) ⇒ Array

Run swiftlint on all files and returns the issues

Returns:

  • (Array)

    swiftlint issues



166
167
168
169
170
171
172
173
# File 'lib/danger_plugin.rb', line 166

def run_swiftlint(options, additional_swiftlint_args)
  result = swiftlint.lint(options, additional_swiftlint_args)
  if result == ''
    {}
  else
    JSON.parse(result).flatten
  end
end

#run_swiftlint_for_each(files, options, additional_swiftlint_args) ⇒ Array

Run swiftlint on each file and aggregate collect the issues

Returns:

  • (Array)

    swiftlint issues



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/danger_plugin.rb', line 178

def run_swiftlint_for_each(files, options, additional_swiftlint_args)
  # Use `--use-script-input-files` flag along with `SCRIPT_INPUT_FILE_#` ENV
  # variables to pass the list of files we want swiftlint to lint
  options.merge!(use_script_input_files: true)

  # Set environment variables:
  #   * SCRIPT_INPUT_FILE_COUNT equal to number of files
  #   * a variable in the form of SCRIPT_INPUT_FILE_# for each file
  env = script_input(files)

  result = swiftlint.lint(options, additional_swiftlint_args, env)
  if result == ''
    {}
  else
    JSON.parse(result).flatten
  end
end

#script_input(files) ⇒ Hash

Converts an array of files into ‘SCRIPT_INPUT_FILE_#` format for use with `–use-script-input-files`

Returns:

  • (Hash)

    mapping from ‘SCRIPT_INPUT_FILE_#` to file SCRIPT_INPUT_FILE_COUNT will be set to the number of files



200
201
202
203
204
205
# File 'lib/danger_plugin.rb', line 200

def script_input(files)
  files
    .map.with_index { |file, i| ["SCRIPT_INPUT_FILE_#{i}", file.to_s] }
    .push(['SCRIPT_INPUT_FILE_COUNT', files.size.to_s])
    .to_h
end

#send_inline_comment(results, method) ⇒ void

This method returns an undefined value.

Send inline comment with danger’s warn or fail method



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/danger_plugin.rb', line 298

def send_inline_comment(results, method)
  dir = "#{Dir.pwd}/"
  results.each do |r|
    github_filename = r['file'].gsub(dir, '')
    message = "#{r['reason']}".dup

    # extended content here
    filename = r['file'].split('/').last
    message << "\n"
    message << "`#{r['rule_id']}`" # helps writing exceptions // swiftlint:disable:this rule_id
    message << " `#{filename}:#{r['line']}`" # file:line for pasting into Xcode Quick Open
    
    send(method, message, file: github_filename, line: r['line'])
  end
end

#swiftlintSwiftLint

Make SwiftLint object for binary_path

Returns:

  • (SwiftLint)


322
323
324
# File 'lib/danger_plugin.rb', line 322

def swiftlint
  Swiftlint.new(binary_path)
end