├── LICENSE ├── README.md └── unused.rb /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Paul Taykalo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unused 2 | `unused.rb` Searches for unused swift functions, and variable at specified path 3 | 4 | ## Usage 5 | ``` 6 | cd 7 | /unused.rb 8 | ``` 9 | 10 | ## Output 11 | ``` 12 | Item< func loadWebViewTos [private] from:File.swift:23:0> 13 | Total items to be checked 4276 14 | Total unique items to be checked 1697 15 | Starting searching globally it can take a while 16 | Item< func applicationHasUnitTestTargetInjected [] from:AnotherFile.swift:31:0> 17 | Item< func getSelectedIds [] from: AnotherFile.swift:82:0> 18 | ``` 19 | 20 | ## Xcode integration 21 | In order to integrate this to Xcode just add *Custom Build Phase/Run Script* 22 | `~/Projects/swift-scripts/unused.rb xcode` 23 | ![](https://user-images.githubusercontent.com/119268/32348473-88080ed2-c01c-11e7-9de6-762aeb195156.png) 24 | ![](https://user-images.githubusercontent.com/119268/32348476-8af3a700-c01c-11e7-893f-013851568882.png) 25 | 26 | ## Known issues: 27 | - Fully text search (no fancy stuff) 28 | - A lot of false-positives (protocols, functions, objc interoop, System delegate methods) 29 | - A lot of false-negatives (text search, yep) 30 | -------------------------------------------------------------------------------- /unused.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | #encoding: utf-8 3 | Encoding.default_external = Encoding::UTF_8 4 | Encoding.default_internal = Encoding::UTF_8 5 | 6 | class Item 7 | def initialize(file, line, at) 8 | @file = file 9 | @line = line 10 | @at = at + 1 11 | if match = line.match(/(func|let|var|class|enum|struct|protocol)\s+(\w+)/) 12 | @type = match.captures[0] 13 | @name = match.captures[1] 14 | end 15 | end 16 | 17 | def modifiers 18 | return @modifiers if @modifiers 19 | @modifiers = [] 20 | if match = @line.match(/(.*?)#{@type}/) 21 | @modifiers = match.captures[0].split(" ") 22 | end 23 | return @modifiers 24 | end 25 | 26 | def name 27 | @name 28 | end 29 | 30 | def file 31 | @file 32 | end 33 | 34 | def to_s 35 | serialize 36 | end 37 | def to_str 38 | serialize 39 | end 40 | 41 | def full_file_path 42 | Dir.pwd + '/' + @file 43 | end 44 | 45 | def serialize 46 | "Item< #{@type.to_s.green} #{@name.to_s.yellow} [#{modifiers.join(" ").cyan}] from: #{@file}:#{@at}:0>" 47 | end 48 | 49 | def to_xcode 50 | "#{full_file_path}:#{@at}:0: warning: #{@type.to_s} #{@name.to_s} is unused" 51 | end 52 | 53 | 54 | end 55 | class Unused 56 | def find 57 | items = [] 58 | all_files = Dir.glob("**/*.swift").reject do |path| 59 | File.directory?(path) 60 | end 61 | 62 | all_files.each { |my_text_file| 63 | file_items = grab_items(my_text_file) 64 | file_items = filter_items(file_items) 65 | 66 | non_private_items, private_items = file_items.partition { |f| !f.modifiers.include?("private") && !f.modifiers.include?("fileprivate") } 67 | items += non_private_items 68 | 69 | # Usage within the file 70 | if private_items.length > 0 71 | find_usages_in_files([my_text_file], [], private_items) 72 | end 73 | 74 | } 75 | 76 | puts "Total items to be checked #{items.length}" 77 | 78 | items = items.uniq { |f| f.name } 79 | puts "Total unique items to be checked #{items.length}" 80 | 81 | puts "Starting searching globally it can take a while".green 82 | 83 | xibs = Dir.glob("**/*.xib") 84 | storyboards = Dir.glob("**/*.storyboard") 85 | 86 | find_usages_in_files(all_files, xibs + storyboards, items) 87 | 88 | end 89 | 90 | def ignore_files_with_regexps(files, regexps) 91 | files.select { |f| regexps.all? { |r| Regexp.new(r).match(f.file).nil? } } 92 | end 93 | 94 | def ignoring_regexps_from_command_line_args 95 | regexps = [] 96 | should_skip_predefined_ignores = false 97 | 98 | arguments = ARGV.clone 99 | until arguments.empty? 100 | item = arguments.shift 101 | if item == "--ignore" 102 | regex = arguments.shift 103 | regexps += [regex] 104 | end 105 | 106 | if item == "--skip-predefined-ignores" 107 | should_skip_predefined_ignores = true 108 | end 109 | end 110 | 111 | if not should_skip_predefined_ignores 112 | regexps += [ 113 | "^Pods/", 114 | "fastlane/", 115 | "Tests.swift$", 116 | "Spec.swift$", 117 | "Tests/" 118 | ] 119 | end 120 | 121 | regexps 122 | end 123 | 124 | def find_usages_in_files(files, xibs, items_in) 125 | items = items_in 126 | usages = items.map { |f| 0 } 127 | files.each { |file| 128 | lines = File.readlines(file).map {|line| line.gsub(/^[^\/]*\/\/.*/, "") } 129 | words = lines.join("\n").split(/\W+/) 130 | words_arrray = words.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten 131 | 132 | wf = Hash[*words_arrray] 133 | 134 | items.each_with_index { |f, i| 135 | usages[i] += (wf[f.name] || 0) 136 | } 137 | # Remove all items which has usage 2+ 138 | indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } 139 | 140 | # reduce usage array if we found some functions already 141 | indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } 142 | } 143 | 144 | xibs.each { |xib| 145 | lines = File.readlines(xib).map {|line| line.gsub(/^\s*\/\/.*/, "") } 146 | full_xml = lines.join(" ") 147 | classes = full_xml.scan(/(class|customClass)="([^"]+)"/).map { |cd| cd[1] } 148 | classes_array = classes.group_by { |w| w }.map { |w, ws| [w, ws.length] }.flatten 149 | 150 | wf = Hash[*classes_array] 151 | 152 | items.each_with_index { |f, i| 153 | usages[i] += (wf[f.name] || 0) 154 | } 155 | # Remove all items which has usage 2+ 156 | indexes = usages.each_with_index.select { |u, i| u >= 2 }.map { |f, i| i } 157 | 158 | # reduce usage array if we found some functions already 159 | indexes.reverse.each { |i| usages.delete_at(i) && items.delete_at(i) } 160 | 161 | } 162 | 163 | regexps = ignoring_regexps_from_command_line_args() 164 | 165 | items = ignore_files_with_regexps(items, regexps) 166 | 167 | if items.length > 0 168 | if ARGV[0] == "xcode" 169 | $stderr.puts "#{items.map { |e| e.to_xcode }.join("\n")}" 170 | else 171 | puts "#{items.map { |e| e.to_s }.join("\n ")}" 172 | end 173 | end 174 | end 175 | 176 | def grab_items(file) 177 | lines = File.readlines(file).map {|line| line.gsub(/^\s*\/\/.*/, "") } 178 | 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)} 179 | end 180 | 181 | def filter_items(items) 182 | items.select { |f| 183 | !f.name.start_with?("test") && !f.modifiers.include?("@IBAction") && !f.modifiers.include?("override") && !f.modifiers.include?("@objc") && !f.modifiers.include?("@IBInspectable") 184 | } 185 | end 186 | 187 | end 188 | 189 | class String 190 | def black; "\e[30m#{self}\e[0m" end 191 | def red; "\e[31m#{self}\e[0m" end 192 | def green; "\e[32m#{self}\e[0m" end 193 | def yellow; "\e[33m#{self}\e[0m" end 194 | def blue; "\e[34m#{self}\e[0m" end 195 | def magenta; "\e[35m#{self}\e[0m" end 196 | def cyan; "\e[36m#{self}\e[0m" end 197 | def gray; "\e[37m#{self}\e[0m" end 198 | 199 | def bg_black; "\e[40m#{self}\e[0m" end 200 | def bg_red; "\e[41m#{self}\e[0m" end 201 | def bg_green; "\e[42m#{self}\e[0m" end 202 | def bg_brown; "\e[43m#{self}\e[0m" end 203 | def bg_blue; "\e[44m#{self}\e[0m" end 204 | def bg_magenta; "\e[45m#{self}\e[0m" end 205 | def bg_cyan; "\e[46m#{self}\e[0m" end 206 | def bg_gray; "\e[47m#{self}\e[0m" end 207 | 208 | def bold; "\e[1m#{self}\e[22m" end 209 | def italic; "\e[3m#{self}\e[23m" end 210 | def underline; "\e[4m#{self}\e[24m" end 211 | def blink; "\e[5m#{self}\e[25m" end 212 | def reverse_color; "\e[7m#{self}\e[27m" end 213 | end 214 | 215 | 216 | Unused.new.find --------------------------------------------------------------------------------