From eb48b72b0f90d0e3a990fcd71909a50329cd1fc4 Mon Sep 17 00:00:00 2001 From: Carlos Aguiar Date: Wed, 27 Jan 2021 00:12:04 +0000 Subject: [PATCH 1/2] Implemented colors asset checker --- Classes/main.swift | 85 +++++++++++++++++++ .../AssetChecker.xcodeproj/project.pbxproj | 2 +- .../AssetChecker/Base.lproj/Main.storyboard | 22 +++-- .../Images.xcassets/Contents.json | 6 ++ .../UnusedColor.colorset/Contents.json | 38 +++++++++ run | 7 +- 6 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 Example/AssetChecker/Images.xcassets/Contents.json create mode 100644 Example/AssetChecker/Images.xcassets/UnusedColor.colorset/Contents.json diff --git a/Classes/main.swift b/Classes/main.swift index c9cda15..a621c66 100755 --- a/Classes/main.swift +++ b/Classes/main.swift @@ -5,6 +5,7 @@ import Foundation // Configure me \o/ var sourcePathOption:String? = nil var assetCatalogPathOption:String? = nil +var colorAssetCatalogPathOption:String? = nil let ignoredUnusedNames = [String]() for (index, arg) in CommandLine.arguments.enumerated() { @@ -13,6 +14,9 @@ for (index, arg) in CommandLine.arguments.enumerated() { sourcePathOption = arg case 2: assetCatalogPathOption = arg + colorAssetCatalogPathOption = arg + case 3: + colorAssetCatalogPathOption = arg default: break } @@ -28,6 +32,11 @@ guard let assetCatalogAbsolutePath = assetCatalogPathOption else { exit(0) } +guard let colorAssetCatalogAbsolutePath = colorAssetCatalogPathOption else { + print("AssetChecker:: error: Color Asset Catalog path was missing!") + exit(0) +} + print("Searching sources in \(sourcePath) for assets in \(assetCatalogAbsolutePath)") /* Put here the asset generating false positives, @@ -133,3 +142,79 @@ broken.forEach { print("\(assetCatalogAbsolutePath):: error: [Asset Missing] \($ if broken.count > 0 { exit(1) } + + +// MARK: Colors +func listColorAssets() -> [String] { + let extensionName = "colorset" + let enumerator = FileManager.default.enumerator(atPath: colorAssetCatalogAbsolutePath) + return elementsInEnumerator(enumerator) + .filter { $0.hasSuffix(extensionName) } // Is Asset + .map { $0.replacingOccurrences(of: ".\(extensionName)", with: "") } // Remove extension + .map { $0.components(separatedBy: "/").last ?? $0 } // Remove folder path +} + +func colorsStrings(inStringFile: String) -> [String] { + var localizedStrings = [String]() + let namePattern = "([\\w-]+)" + let patterns = [ + "#colorLiteral\\(resourceName: \"\(namePattern)\"\\)", // Image Literal + "UIColor\\(named:\\s*\"\(namePattern)\"\\)", // Default UIImage call (Swift) + "UIColor colorNamed:\\s*\\@\"\(namePattern)\"", // Default UIImage call + "\\ [String] { + let enumerator = FileManager.default.enumerator(atPath:sourcePath) + print(sourcePath) + + #if swift(>=4.1) + return elementsInEnumerator(enumerator) + .filter { $0.hasSuffix(".m") || $0.hasSuffix(".swift") || $0.hasSuffix(".xib") || $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files + .map { "\(sourcePath)/\($0)" } // Build file paths + .map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents + .compactMap{$0} + .compactMap{$0} // Remove nil entries + .map(colorsStrings) // Find localizedStrings ocurrences + .flatMap{$0} // Flatten + #else + return elementsInEnumerator(enumerator) + .filter { $0.hasSuffix(".m") || $0.hasSuffix(".swift") || $0.hasSuffix(".xib") || $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files + .map { "\(sourcePath)/\($0)" } // Build file paths + .map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents + .flatMap{$0} + .flatMap{$0} // Remove nil entries + .map(colorsStrings) // Find localizedStrings ocurrences + .flatMap{$0} // Flatten + #endif +} + +print("Searching colors in \(sourcePath) for color assets in \(colorAssetCatalogAbsolutePath)") + +let colors = Set(listColorAssets()) +let usedColors = Set(listUsedColorsAssetLiterals()) + +// Generate Warnings for Unused Assets +let unusedColors = colors.subtracting(usedColors) +unusedColors.forEach { print("\(colorAssetCatalogAbsolutePath):: warning: [Color Unused] \($0)") } + +// Generate Error for broken Assets +let brokenColors = usedColors.subtracting(colors) +brokenColors.forEach { print("\(assetCatalogAbsolutePath):: error: [Color Missing] \($0)") } + +if brokenColors.count > 0 { + exit(1) +} diff --git a/Example/AssetChecker.xcodeproj/project.pbxproj b/Example/AssetChecker.xcodeproj/project.pbxproj index e323d60..73b94ca 100644 --- a/Example/AssetChecker.xcodeproj/project.pbxproj +++ b/Example/AssetChecker.xcodeproj/project.pbxproj @@ -362,7 +362,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "${PODS_ROOT}/AssetChecker/run --catalog ${SRCROOT}/Resource/Images.xcassets"; + shellScript = "${PODS_ROOT}/AssetChecker/run --catalog ${SRCROOT}/AssetChecker/Images.xcassets --colors ${SRCROOT}/AssetChecker/Images.xcassets\n"; }; CD3F94CDB64297E52FBE5D36 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/Example/AssetChecker/Base.lproj/Main.storyboard b/Example/AssetChecker/Base.lproj/Main.storyboard index 1b5cfbe..de934fd 100644 --- a/Example/AssetChecker/Base.lproj/Main.storyboard +++ b/Example/AssetChecker/Base.lproj/Main.storyboard @@ -1,11 +1,10 @@ - - - - + + - + + @@ -22,13 +21,14 @@ - + - + - + + @@ -44,6 +44,12 @@ + + + + + + diff --git a/Example/AssetChecker/Images.xcassets/Contents.json b/Example/AssetChecker/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/AssetChecker/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/AssetChecker/Images.xcassets/UnusedColor.colorset/Contents.json b/Example/AssetChecker/Images.xcassets/UnusedColor.colorset/Contents.json new file mode 100644 index 0000000..22c4bb0 --- /dev/null +++ b/Example/AssetChecker/Images.xcassets/UnusedColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/run b/run index d056885..e97839a 100755 --- a/run +++ b/run @@ -8,6 +8,7 @@ swiftFile="${DIR}/Classes/main.swift" SOURCE=$SRCROOT CATALOG="" +COLORS_CATALOG="" while [[ $# > 0 ]] do @@ -22,6 +23,10 @@ case $key in CATALOG="$2" shift # past argument ;; + --colors) + COLORS_CATALOG="$2" + shift # past argument + ;; *) # unknown option @@ -30,6 +35,6 @@ esac shift # past argument or value done -cmd="$swiftFile $SOURCE $CATALOG" +cmd="$swiftFile $SOURCE $CATALOG $COLORS_CATALOG" echo $cmd $cmd From 6ec89ec83e6b26eb23e7ea026db55f10f820761a Mon Sep 17 00:00:00 2001 From: Carlos Aguiar Date: Tue, 9 Feb 2021 23:59:30 +0000 Subject: [PATCH 2/2] Fixed some PR comments --- Classes/main.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Classes/main.swift b/Classes/main.swift index a621c66..23e1296 100755 --- a/Classes/main.swift +++ b/Classes/main.swift @@ -154,7 +154,7 @@ func listColorAssets() -> [String] { .map { $0.components(separatedBy: "/").last ?? $0 } // Remove folder path } -func colorsStrings(inStringFile: String) -> [String] { +func colorStrings(inStringFile: String) -> [String] { var localizedStrings = [String]() let namePattern = "([\\w-]+)" let patterns = [ @@ -177,7 +177,7 @@ func colorsStrings(inStringFile: String) -> [String] { return localizedStrings } -func listUsedColorsAssetLiterals() -> [String] { +func listUsedColorAssetLiterals() -> [String] { let enumerator = FileManager.default.enumerator(atPath:sourcePath) print(sourcePath) @@ -186,18 +186,16 @@ func listUsedColorsAssetLiterals() -> [String] { .filter { $0.hasSuffix(".m") || $0.hasSuffix(".swift") || $0.hasSuffix(".xib") || $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files .map { "\(sourcePath)/\($0)" } // Build file paths .map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents - .compactMap{$0} .compactMap{$0} // Remove nil entries - .map(colorsStrings) // Find localizedStrings ocurrences + .map(colorStrings) // Find localizedStrings ocurrences .flatMap{$0} // Flatten #else return elementsInEnumerator(enumerator) .filter { $0.hasSuffix(".m") || $0.hasSuffix(".swift") || $0.hasSuffix(".xib") || $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files .map { "\(sourcePath)/\($0)" } // Build file paths .map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents - .flatMap{$0} .flatMap{$0} // Remove nil entries - .map(colorsStrings) // Find localizedStrings ocurrences + .map(colorStrings) // Find localizedStrings ocurrences .flatMap{$0} // Flatten #endif } @@ -205,7 +203,7 @@ func listUsedColorsAssetLiterals() -> [String] { print("Searching colors in \(sourcePath) for color assets in \(colorAssetCatalogAbsolutePath)") let colors = Set(listColorAssets()) -let usedColors = Set(listUsedColorsAssetLiterals()) +let usedColors = Set(listUsedColorAssetLiterals()) // Generate Warnings for Unused Assets let unusedColors = colors.subtracting(usedColors)