mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
d305e03905
Current events have renewed the conversation in our community about the roles of terminology with racist connotations in our software. Many companies and developers are now taking appropriate steps to remove this terminology from their codebases and products. (e.g. [GitHub](https://twitter.com/natfriedman/status/1271253144442253312)) This small rule prevents the use of declarations that contain any of the terms: whitelist, blacklist, master, and slave. It may be appropriate to add more terms to this list now or in the future.
331 lines
8.9 KiB
Ruby
Executable File
331 lines
8.9 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
################################
|
|
# Requires
|
|
################################
|
|
|
|
require 'fileutils'
|
|
require 'open3'
|
|
require 'optparse'
|
|
require 'erb'
|
|
|
|
################################
|
|
# Options
|
|
################################
|
|
|
|
@options = {
|
|
branch: 'HEAD',
|
|
iterations: 5,
|
|
skip_clean: false,
|
|
verbose: false
|
|
}
|
|
|
|
OptionParser.new do |opts|
|
|
opts.on('--branch BRANCH', "compares the performance of BRANCH against 'master'") do |branch|
|
|
@options[:branch] = branch
|
|
end
|
|
opts.on('--iterations N', Integer, 'iterates lint N times on each repositories') do |iterations|
|
|
@options[:iterations] = iterations
|
|
end
|
|
opts.on('--skip-clean', 'skip cleaning on completion') do |skip_clean|
|
|
@options[:skip_clean] = skip_clean
|
|
end
|
|
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
|
|
@options[:verbose] = v
|
|
end
|
|
end.parse!
|
|
|
|
################################
|
|
# Classes
|
|
################################
|
|
|
|
class Repo
|
|
attr_accessor :name
|
|
attr_accessor :github_location
|
|
attr_accessor :commit_hash
|
|
attr_accessor :branch_exit_value
|
|
attr_accessor :branch_duration
|
|
attr_accessor :master_exit_value
|
|
attr_accessor :master_duration
|
|
|
|
def initialize(name, github_location)
|
|
@name = name
|
|
@github_location = github_location
|
|
end
|
|
|
|
def git_url
|
|
"https://github.com/#{github_location}"
|
|
end
|
|
|
|
def to_s
|
|
@name
|
|
end
|
|
|
|
def duration_report
|
|
percent_change = 100 * (@master_duration - @branch_duration) / @master_duration
|
|
faster_slower = nil
|
|
if @branch_duration < @master_duration
|
|
faster_slower = 'faster'
|
|
else
|
|
faster_slower = 'slower'
|
|
percent_change *= -1
|
|
end
|
|
"Linting #{self} with this PR took #{@branch_duration}s " \
|
|
"vs #{@master_duration}s on master (#{percent_change.to_i}\% #{faster_slower})"
|
|
end
|
|
end
|
|
|
|
################################
|
|
# Methods
|
|
################################
|
|
|
|
def message(str)
|
|
$stderr.puts('Message: ' + str)
|
|
end
|
|
|
|
def warn(str)
|
|
$stderr.puts('Warning: ' + str)
|
|
end
|
|
|
|
def fail(str)
|
|
$stderr.puts('Error: ' + str)
|
|
exit
|
|
end
|
|
|
|
def perform(*args)
|
|
commands = args
|
|
if @options[:verbose]
|
|
commands.each do |x|
|
|
puts(x)
|
|
system(x)
|
|
end
|
|
else
|
|
commands.each { |x| `#{x}` }
|
|
end
|
|
end
|
|
|
|
def validate_state_to_run
|
|
if `git symbolic-ref HEAD --short || true`.strip == 'master' && @options[:branch] == 'HEAD'
|
|
fail "can't run osscheck without '--branch' option from 'master' as the script compares " \
|
|
"the performance of this branch against 'master'"
|
|
end
|
|
end
|
|
|
|
def make_directory_structure
|
|
['branch_reports', 'master_reports'].each do |dir|
|
|
FileUtils.mkdir_p("#{@working_dir}/#{dir}")
|
|
end
|
|
end
|
|
|
|
def convert_to_link(repo, string)
|
|
string = remove_base_path(repo, string)
|
|
string.sub!('.swift:', '.swift#L')
|
|
string = string.partition(': warning:').first.partition(': error:').first
|
|
"#{repo.git_url}/blob/#{repo.commit_hash}#{string}"
|
|
end
|
|
|
|
def remove_base_path(repo, string)
|
|
string.sub("#{Dir.pwd}/#{@working_dir}/#{repo.name}", '')
|
|
end
|
|
|
|
def non_empty_lines(path)
|
|
File.read(path).split(/\n+/).reject(&:empty?)
|
|
end
|
|
|
|
def setup_repos
|
|
@repos.each do |repo|
|
|
dir = "#{@working_dir}/#{repo.name}"
|
|
puts "Cloning #{repo}"
|
|
perform("git clone #{repo.git_url} --depth 1 #{dir} 2> /dev/null")
|
|
swiftlint_config = "#{dir}/.swiftlint.yml"
|
|
FileUtils.rm_rf(swiftlint_config)
|
|
if repo.name == 'Swift'
|
|
File.open(swiftlint_config, 'w') do |file|
|
|
file.puts('included: stdlib')
|
|
end
|
|
end
|
|
if @only_rules_changed && @rules_changed
|
|
File.open(swiftlint_config, 'w') do |file|
|
|
file.puts('only_rules:')
|
|
file.puts(@rules_changed.map { |rule| " - #{rule}" })
|
|
end
|
|
end
|
|
Dir.chdir(dir) do
|
|
repo.commit_hash = `git rev-parse HEAD`.strip
|
|
end
|
|
end
|
|
end
|
|
|
|
def generate_reports(branch)
|
|
@repos.each do |repo|
|
|
Dir.chdir("#{@working_dir}/#{repo.name}") do
|
|
iterations = @options[:iterations]
|
|
print "Linting #{iterations} iterations of #{repo} with #{branch}: 1"
|
|
durations = []
|
|
start = Time.now
|
|
command = "../builds/.build/release/swiftlint lint --no-cache #{'--enable-all-rules' unless @only_rules_changed} --reporter xcode"
|
|
File.open("../#{branch}_reports/#{repo}.txt", 'w') do |file|
|
|
puts "\n#{command}" if @options[:verbose]
|
|
Open3.popen2(command) do |_, stdout, wait_thr|
|
|
while line = stdout.gets
|
|
file.puts line
|
|
end
|
|
if branch == 'branch'
|
|
repo.branch_exit_value = wait_thr.value
|
|
else
|
|
repo.master_exit_value = wait_thr.value
|
|
end
|
|
end
|
|
end
|
|
durations << Time.now - start
|
|
for i in 2..iterations
|
|
print "..#{i}"
|
|
start = Time.now
|
|
puts command if @options[:verbose]
|
|
Open3.popen2(command) { |_, stdout, _| stdout.read }
|
|
durations << Time.now - start
|
|
end
|
|
puts ''
|
|
average_duration = (durations.reduce(:+) / iterations).round(2)
|
|
if branch == 'branch'
|
|
repo.branch_duration = average_duration
|
|
else
|
|
repo.master_duration = average_duration
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def build(branch)
|
|
puts "Building #{branch}"
|
|
|
|
dir = "#{@working_dir}/builds"
|
|
target = branch == 'master' ? @effective_master_commitish : @options[:branch]
|
|
if File.directory?(dir)
|
|
perform("cd #{dir}; git checkout #{target}")
|
|
else
|
|
perform("git fetch && git worktree add --detach #{dir} #{target}")
|
|
end
|
|
|
|
Dir.chdir(dir) do
|
|
FileUtils.rm_rf %w[Packages .build]
|
|
end
|
|
|
|
build_command = "cd #{dir}; swift build -c release"
|
|
|
|
perform(build_command)
|
|
return if $?.success?
|
|
|
|
return_value = nil
|
|
puts build_command if @options[:verbose]
|
|
Open3.popen3(build_command) do |_, stdout, _, wait_thr|
|
|
puts stdout.read.chomp
|
|
return_value = wait_thr.value
|
|
end
|
|
|
|
fail "Could not build #{branch}" unless return_value.success?
|
|
end
|
|
|
|
def diff_and_report_changes_to_danger
|
|
@repos.each { |repo| message repo.duration_report }
|
|
|
|
@repos.each do |repo|
|
|
if repo.master_exit_value != repo.branch_exit_value
|
|
warn "This PR changed the exit value when running on #{repo.name}: " \
|
|
"(#{repo.master_exit_value} to #{repo.branch_exit_value})"
|
|
# If the exit value changed, don't show the fixes or regressions for this
|
|
# repo because it's likely due to a crash, and all violations would be noisy
|
|
next
|
|
end
|
|
|
|
branch = non_empty_lines("#{@working_dir}/branch_reports/#{repo.name}.txt")
|
|
master = non_empty_lines("#{@working_dir}/master_reports/#{repo.name}.txt")
|
|
|
|
(master - branch).each do |fixed|
|
|
escaped_message = ERB::Util.html_escape remove_base_path(repo, fixed)
|
|
message "This PR fixed a violation in #{repo.name}: [#{escaped_message}](#{convert_to_link(repo, fixed)})"
|
|
end
|
|
(branch - master).each do |violation|
|
|
escaped_message = ERB::Util.html_escape remove_base_path(repo, violation)
|
|
warn "This PR introduced a violation in #{repo.name}: [#{escaped_message}](#{convert_to_link(repo, violation)})"
|
|
end
|
|
end
|
|
end
|
|
|
|
def fetch_origin
|
|
perform('git fetch origin')
|
|
end
|
|
|
|
def clean_up
|
|
FileUtils.rm_rf(@working_dir)
|
|
perform('git worktree prune')
|
|
end
|
|
|
|
def set_globals
|
|
@effective_master_commitish = `git merge-base origin/master #{@options[:branch]}`.chomp
|
|
@changed_swift_files = `git diff --diff-filter=d #{@effective_master_commitish} --name-only | grep "\.swift$" || true`.split("\n")
|
|
@changed_rule_files = @changed_swift_files.select do |file|
|
|
file.start_with? 'Source/SwiftLintFramework/Rules/'
|
|
end
|
|
@rules_changed = @changed_rule_files.map do |path|
|
|
if File.read(path) =~ /^\s+identifier: "(\w+)",$/
|
|
$1
|
|
else
|
|
nil
|
|
end
|
|
end.compact.sort
|
|
# True iff the only Swift files that were changed are SwiftLint rules, and that number is one or greater
|
|
@only_rules_changed = !@rules_changed.empty? && @changed_swift_files.count == @rules_changed.count
|
|
end
|
|
|
|
def print_rules_changed
|
|
if @only_rules_changed
|
|
puts "Only #{@rules_changed.count} rules changed: #{@rules_changed.join(', ')}"
|
|
end
|
|
end
|
|
|
|
################################
|
|
# Script
|
|
################################
|
|
|
|
# Constants
|
|
@working_dir = 'osscheck'
|
|
@repos = [
|
|
Repo.new('Aerial', 'JohnCoates/Aerial'),
|
|
Repo.new('Alamofire', 'Alamofire/Alamofire'),
|
|
Repo.new('Firefox', 'mozilla-mobile/firefox-ios'),
|
|
Repo.new('Kickstarter', 'kickstarter/ios-oss'),
|
|
Repo.new('Moya', 'Moya/Moya'),
|
|
Repo.new('Nimble', 'Quick/Nimble'),
|
|
Repo.new('Quick', 'Quick/Quick'),
|
|
Repo.new('Realm', 'realm/realm-cocoa'),
|
|
Repo.new('SourceKitten', 'jpsim/SourceKitten'),
|
|
Repo.new('Sourcery', 'krzysztofzablocki/Sourcery'),
|
|
Repo.new('Swift', 'apple/swift'),
|
|
Repo.new('WordPress', 'wordpress-mobile/WordPress-iOS')
|
|
]
|
|
|
|
# Clean up
|
|
clean_up unless @options[:skip_clean]
|
|
|
|
# Prep
|
|
$stdout.sync = true
|
|
validate_state_to_run
|
|
set_globals
|
|
print_rules_changed
|
|
setup_repos
|
|
make_directory_structure
|
|
fetch_origin
|
|
|
|
# Build & generate reports for branch & master
|
|
%w[branch master].each do |branch|
|
|
build(branch)
|
|
generate_reports(branch)
|
|
end
|
|
|
|
# Diff and report changes to Danger
|
|
diff_and_report_changes_to_danger
|
|
|
|
# Clean up
|
|
clean_up unless @options[:skip_clean]
|