diff --git a/xcode/UnusedConfig.yml b/xcode/UnusedConfig.yml new file mode 100644 index 0000000..619dd71 --- /dev/null +++ b/xcode/UnusedConfig.yml @@ -0,0 +1,6 @@ +excluded-resources: + - Generated/ + - Resources/Localization + - Carthage/ + - Pods/ + - FormattingService.swift \ No newline at end of file diff --git a/xcode/build_phases/Unused.rb b/xcode/build_phases/Unused.rb new file mode 100644 index 0000000..478a8d2 --- /dev/null +++ b/xcode/build_phases/Unused.rb @@ -0,0 +1,189 @@ +# source: https://github.com/PaulTaykalo/swift-scripts/blob/master/unused.rb +#!/usr/bin/ruby +#encoding: utf-8 +require 'yaml' +require 'optparse' + +Encoding.default_external = Encoding::UTF_8 +Encoding.default_internal = Encoding::UTF_8 + +class Item + def initialize(file, line, at) + @file = file + @line = line + @at = at + 1 + if match = line.match(/(func|let|var|class|enum|struct|protocol)\s+(\w+)/) + @type = match.captures[0] + @name = match.captures[1] + end + end + + def modifiers + return @modifiers if @modifiers + @modifiers = [] + if match = @line.match(/(.*?)#{@type}/) + @modifiers = match.captures[0].split(" ") + end + return @modifiers + end + + def name + @name + end + + def file + @file + end + + def to_s + serialize + end + def to_str + serialize + end + + def full_file_path + Dir.pwd + '/' + @file + end + + def serialize + "#{@file} has unused \"#{@type.to_s} #{@name.to_s}\"" + end + def to_xcode + "#{full_file_path}:#{@at}:0: warning: #{@type.to_s} #{@name.to_s} is unused" + end + + +end +class Unused + def find + items = [] + unused_warnings = [] + + regexps = parse_arguments + + all_files = Dir.glob("**/*.swift").reject do |path| + File.directory?(path) + end + + all_files.each { |my_text_file| + file_items = grab_items(my_text_file) + file_items = filter_items(file_items) + + non_private_items, private_items = file_items.partition { |f| !f.modifiers.include?("private") && !f.modifiers.include?("fileprivate") } + items += non_private_items + + # Usage within the file + if private_items.length > 0 + unused_warnings += find_usages_in_files([my_text_file], [], private_items, regexps) + end + + } + + xibs = Dir.glob("**/*.xib") + storyboards = Dir.glob("**/*.storyboard") + + unused_warnings += find_usages_in_files(all_files, xibs + storyboards, items, regexps) + + if unused_warnings.length > 0 + # show warning + puts "Unused Code Warning!: warning: Total Count #{unused_warnings.length}" + puts "#{unused_warnings.map { |e| e.to_xcode}.join("\n")}" + # write log + File.open("UnusedLog.txt", "w") do |file| + file.write("Unused code warnings count: #{unused_warnings.length}\n\n") + file.write("#{unused_warnings.map { |e| e.serialize }.join("\n")}") + end + end + end + + def parse_arguments + resources = [] + + options = {} + OptionParser.new do |opts| + options[:exclude] = [] + + opts.on("-c", "--config=FileName") { |c| options[:config] = c } + opts.on("-i", "--exclude [a, b, c]", Array) { |i| options[:exclude] += i if !i.nil? } + + end.parse! + + # find --config file + if !options[:config].nil? + fileName = options[:config] + resources += YAML.load_file(fileName).fetch("excluded-resources") + elsif + puts "---------\n Warning: Config file is not provided \n---------" + end + + # find --exclude files + if !options[:exclude].nil? + resources += options[:exclude] + end + + # create and return Regexp + resources.map { |r| Regexp.new(r) } + end + + def grab_items(file) + lines = File.readlines(file).map {|line| line.gsub(/^\s*\/\/.*/, "") } + items = lines.each_with_index.select { |line, i| line[/(func|let|var|class|enum|struct|protocol)\s+\w+/] }.map { |line, i| Item.new(file, line, i)} + end + + def filter_items(items) + items.select { |f| + !f.name.start_with?("test") && !f.modifiers.include?("@IBAction") && !f.modifiers.include?("override") && !f.modifiers.include?("@objc") && !f.modifiers.include?("@IBInspectable") + } + end + + # remove files, that maches excluded Regexps array + def ignore_files_with_regexps(files, regexps) + files.select { |f| regexps.all? { |r| r.match(f.file).nil? } } + end + + def find_usages_in_files(files, xibs, items_in, regexps) + items = items_in + usages = items.map { |f| 0 } + files.each { |file| + lines = File.readlines(file).map {|line| line.gsub(/^[^\/]*\/\/.*/, "") } + words = lines.join("\n").split(/\W+/) + words_arrray = words.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten + + wf = Hash[*words_arrray] + + items.each_with_index { |f, i| + usages[i] += (wf[f.name] || 0) + } + # Remove all items which has usage 2+ + indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } + + # reduce usage array if we found some functions already + indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } + } + + xibs.each { |xib| + lines = File.readlines(xib).map {|line| line.gsub(/^\s*\/\/.*/, "") } + full_xml = lines.join(" ") + classes = full_xml.scan(/(class|customClass)="([^"]+)"/).map { |cd| cd[1] } + classes_array = classes.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten + + wf = Hash[*classes_array] + + items.each_with_index { |f, i| + usages[i] += (wf[f.name] || 0) + } + # Remove all items which has usage 2+ + indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } + + # reduce usage array if we found some functions already + indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } + + } + + items = ignore_files_with_regexps(items, regexps) + end +end + + +Unused.new.find diff --git a/xcode/build_phases/unused.sh b/xcode/build_phases/unused.sh new file mode 100644 index 0000000..5a0a906 --- /dev/null +++ b/xcode/build_phases/unused.sh @@ -0,0 +1,4 @@ +arguments=("$@") +ignored_files=$(IFS=, ; echo "${arguments[*]}") + +ruby ${PROJECT_DIR}/build-scripts/xcode/build_phases/Unused.rb --config ${PROJECT_DIR}/build-scripts/xcode/UnusedConfig.yml --exclude ${ignored_files} diff --git a/xcode/commonFastfile b/xcode/commonFastfile index 73e596c..ad49254 100644 --- a/xcode/commonFastfile +++ b/xcode/commonFastfile @@ -20,7 +20,7 @@ private_lane :installDependencies do |options| end cocoapods( - repo_update: false + repo_update: true ) end @@ -207,10 +207,26 @@ lane :ManuallyUpdateCodeSigning do |options| git_url = conf.config_file_options[:git_url] shallow_clone = false branch = 'fastlane_certificates' - storage = Match::Storage.for_mode('git', { git_url: git_url, shallow_clone: shallow_clone, git_branch: branch, clone_branch_directly: false}) - storage.download - encryption = Match::Encryption.for_storage_mode('git', { git_url: git_url, working_directory: storage.working_directory}) - encryption.decrypt_files + + storage_conf = lambda do + new_storage = Match::Storage.for_mode('git', { git_url: git_url, shallow_clone: shallow_clone, git_branch: branch, clone_branch_directly: false}) + new_storage.download + return new_storage + end + + encryption_conf_for_storage = lambda do |stor| + new_encryption = Match::Encryption.for_storage_mode('git', { git_url: git_url, working_directory: stor.working_directory}) + new_encryption.decrypt_files + return new_encryption + end + + get_all_files = lambda do |stor| + Dir[File.join(stor.working_directory, "**", "*.{cer,p12,mobileprovision}")] + end + + storage = storage_conf.call + encryption = encryption_conf_for_storage.call(storage) + old_files = get_all_files.call(storage) sh("open #{storage.working_directory}") @@ -220,10 +236,36 @@ lane :ManuallyUpdateCodeSigning do |options| encryption.encrypt_files - files_to_commit = Dir[File.join(storage.working_directory, "**", "*.{cer,p12,mobileprovision}")] + files_to_commit = get_all_files.call(storage) + old_directory = storage.working_directory storage.save_changes!(files_to_commit: files_to_commit) + + + # need to check, because saving changes with delete is another function (update repo if needed) + files_diff = old_files - files_to_commit + + # match can not work with both save/delete functionality `You can't provide both files_to_delete and files_to_commit right now` + # to avoid this we use storage twice if needed + + if files_diff.length > 0 + storage = storage_conf.call + encryption = encryption_conf_for_storage.call(storage) + + files_to_delete = files_diff.map do |file| + old_file = file + old_file.slice! old_directory + new_file = File.join(storage.working_directory, old_file) + File.delete(new_file) if File.exist?(new_file) + file = new_file + end + + encryption.encrypt_files + storage.save_changes!(files_to_delete: files_to_delete) + end + end + def get_keychain_options(options) keychain_name = options[:keychain_name] keychain_password = options[:keychain_password]