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

#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

#file_exists?(paths, file) ⇒ Bool

Return whether the file exists within a specified collection of paths

Returns:

  • (Bool)

    file exists within specified collection of paths



237
238
239
240
241
242
243
# File 'lib/danger_plugin.rb', line 237

def file_exists?(paths, file)
  paths.any? do |path|
    Find.find(path)
        .map { |path_file| Shellwords.escape(path_file) }
        .include?(file)
  end
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



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/danger_plugin.rb', line 183

def find_swift_files(dir_selected, files = nil, excluded_paths = [], included_paths = [])
  # Needs to be escaped before comparsion with escaped file paths
  dir_selected = Shellwords.escape(dir_selected)

  # Assign files to lint
  files = if files.nil?
            (git.modified_files - git.deleted_files) + git.added_files
          else
            Dir.glob(files)
          end
  # Filter files to lint
  files.
    # Ensure only swift files are selected
    select { |file| file.end_with?('.swift') }.
    # Make sure we don't fail when paths have spaces
    map { |file| Shellwords.escape(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| file_exists?(excluded_paths, file) }.
    # Accept files included on configuration
    select do |file|
    next true if included_paths.empty?
    file_exists?(included_paths, 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



248
249
250
251
252
253
254
# File 'lib/danger_plugin.rb', line 248

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

#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.



63
64
65
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
# File 'lib/danger_plugin.rb', line 63

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 = if config_file
                       config_file
                     elsif File.file?('.swiftlint.yml')
                       File.expand_path('.swiftlint.yml')
                     end
  log "Using config file: #{config_file_path}"

  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

  @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



213
214
215
216
217
218
219
220
221
222
# File 'lib/danger_plugin.rb', line 213

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



308
309
310
# File 'lib/danger_plugin.rb', line 308

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

#markdown_issues(results, heading) ⇒ String

Create a markdown table from swiftlint issues

Returns:

  • (String)


259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/danger_plugin.rb', line 259

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



296
297
298
299
# File 'lib/danger_plugin.rb', line 296

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.



225
226
227
228
229
230
231
232
# File 'lib/danger_plugin.rb', line 225

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



158
159
160
161
162
163
164
165
# File 'lib/danger_plugin.rb', line 158

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



170
171
172
173
174
175
176
177
# File 'lib/danger_plugin.rb', line 170

def run_swiftlint_for_each(files, options, additional_swiftlint_args)
  files
    .map { |file| options.merge(path: file) }
    .map { |full_options| swiftlint.lint(full_options, additional_swiftlint_args) }
    .reject { |s| s == '' }
    .map { |s| JSON.parse(s).flatten }
    .flatten
end

#send_inline_comment(results, method) ⇒ void

This method returns an undefined value.

Send inline comment with danger’s warn or fail method



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

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)


304
305
306
# File 'lib/danger_plugin.rb', line 304

def swiftlint
  Swiftlint.new(binary_path)
end