Files
Dalton Claybrook d305e03905 Add inclusive_language rule (#3243)
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.
2020-11-07 22:03:08 -05:00

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]