diff --git a/AbandonedStrings.xcodeproj/project.pbxproj b/AbandonedStrings.xcodeproj/project.pbxproj index f37ef55..a20868c 100644 --- a/AbandonedStrings.xcodeproj/project.pbxproj +++ b/AbandonedStrings.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0361F1401C605FE0009D519A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361F13F1C605FE0009D519A /* main.swift */; }; + E1FCEFA92508F93C00B1138C /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCEFA82508F93C00B1138C /* OutputStream.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -25,6 +26,7 @@ /* Begin PBXFileReference section */ 0361F13C1C605FE0009D519A /* AbandonedStrings */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AbandonedStrings; sourceTree = BUILT_PRODUCTS_DIR; }; 0361F13F1C605FE0009D519A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + E1FCEFA82508F93C00B1138C /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -58,6 +60,7 @@ isa = PBXGroup; children = ( 0361F13F1C605FE0009D519A /* main.swift */, + E1FCEFA82508F93C00B1138C /* OutputStream.swift */, ); path = AbandonedStrings; sourceTree = ""; @@ -89,7 +92,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 1160; ORGANIZATIONNAME = iJoshSmith; TargetAttributes = { 0361F13B1C605FE0009D519A = { @@ -103,6 +106,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 0361F1331C605FE0009D519A; @@ -120,6 +124,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E1FCEFA92508F93C00B1138C /* OutputStream.swift in Sources */, 0361F1401C605FE0009D519A /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -131,17 +136,28 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; @@ -175,17 +191,28 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; @@ -204,22 +231,25 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; }; 0361F1441C605FE0009D519A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_IDENTITY = "-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; 0361F1451C605FE0009D519A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_IDENTITY = "-"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/AbandonedStrings/OutputStream.swift b/AbandonedStrings/OutputStream.swift new file mode 100644 index 0000000..9a84306 --- /dev/null +++ b/AbandonedStrings/OutputStream.swift @@ -0,0 +1,57 @@ +// +// OutputStream.swift +// AbandonedStrings +// +// Created by Andreas Hård on 2020-09-09. +// Copyright © 2020 iJoshSmith. All rights reserved. +// + +import Foundation + +enum OutputStreamType { + case stdOut + case stdErr + + var fileHandle: FileHandle { + switch self { + case .stdOut: + return FileHandle.standardOutput + case .stdErr: + return FileHandle.standardError + } + } +} + +struct OutputStream: TextOutputStream { + let stringEncoding: String.Encoding + let streamType: OutputStreamType + + init(streamType: OutputStreamType, + stringEncoding: String.Encoding) { + self.streamType = streamType + self.stringEncoding = stringEncoding + } + + func write(_ string: String) { + let outputString: String = "\(string)\n" + guard let data = outputString.data(using: stringEncoding) else { + let errorString: String = "Failed to convert string: \"\(string)\" to Data using string encoding: \"\(stringEncoding)\"\n" + forceWriteToStdErr(errorString) + return + } + + streamType.fileHandle.write(data) + } + + func forceWriteToStdErr(_ string: String) { + guard let data = string.data(using: stringEncoding) else { + fatalError("Failed to write to stderr with string: \(string)\n") + } + switch streamType { + case .stdErr: + streamType.fileHandle.write(data) + case .stdOut: + FileHandle.standardError.write(data) + } + } +} diff --git a/AbandonedStrings/main.swift b/AbandonedStrings/main.swift index 9fffaa8..5c62232 100755 --- a/AbandonedStrings/main.swift +++ b/AbandonedStrings/main.swift @@ -17,13 +17,15 @@ import Foundation let dispatchGroup = DispatchGroup.init() let serialWriterQueue = DispatchQueue.init(label: "writer") +let standardOut: OutputStream = OutputStream(streamType: .stdOut, stringEncoding: .utf8) +let standardError: OutputStream = OutputStream(streamType: .stdErr, stringEncoding: .utf8) func findFilesIn(_ directories: [String], withExtensions extensions: [String]) -> [String] { let fileManager = FileManager.default var files = [String]() for directory in directories { guard let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: directory) else { - print("Failed to create enumerator for directory: \(directory)") + standardError.write("Failed to create enumerator for directory: \(directory)") return [] } while let path = enumerator.nextObject() as? String { @@ -41,8 +43,8 @@ func contentsOfFile(_ filePath: String) -> String { do { return try String(contentsOfFile: filePath) } - catch { - print("cannot read file!!!") + catch { + standardError.forceWriteToStdErr("Cannot find file at path: \(filePath)") exit(1) } } @@ -73,7 +75,7 @@ func extractStringIdentifiersFrom(_ stringsFile: String) -> [String] { func extractStringIdentifierFromTrimmedLine(_ line: String) -> String { let indexAfterFirstQuote = line.index(after: line.startIndex) let lineWithoutFirstQuote = line[indexAfterFirstQuote...] - let endIndex = lineWithoutFirstQuote.index(of:"\"")! + let endIndex = lineWithoutFirstQuote.firstIndex(of:"\"")! let identifier = lineWithoutFirstQuote[.. [String]? { c.removeLast() } if isOptionaParameterForWritingAvailable() { - c.remove(at: c.index(of: "write")!) + c.remove(at: c.firstIndex(of: "write")!) } return c } @@ -154,37 +156,37 @@ func isOptionaParameterForWritingAvailable() -> Bool { func displayAbandonedIdentifiersInMap(_ map: StringsFileToAbandonedIdentifiersMap) { for file in map.keys.sorted() { - print("\(file)") + standardOut.write("\(file)") for identifier in map[file]!.sorted() { - print(" \(identifier)") + standardOut.write(" \(identifier)") } - print("") + standardOut.write("") } } if let rootDirectories = getRootDirectories() { - print("Searching for abandoned resource strings…") + standardOut.write("Searching for abandoned resource strings…") let withStoryboard = isOptionalParameterForStoryboardAvailable() let map = findAbandonedIdentifiersIn(rootDirectories, withStoryboard: withStoryboard) if map.isEmpty { - print("No abandoned resource strings were detected.") + standardOut.write("No abandoned resource strings were detected.") } else { - print("Abandoned resource strings were detected:") + standardOut.write("Abandoned resource strings were detected:") displayAbandonedIdentifiersInMap(map) if isOptionaParameterForWritingAvailable() { map.keys.forEach { (stringsFilePath) in - print("\n\nNow modifying \(stringsFilePath) ...") + standardOut.write("\n\nNow modifying \(stringsFilePath) ...") let updatedStringsFileContent = stringsFile(stringsFilePath, without: map[stringsFilePath]!) do { try updatedStringsFileContent.write(toFile: stringsFilePath, atomically: true, encoding: .utf8) } catch { - print("ERROR writing file: \(stringsFilePath)") + standardError.write("ERROR writing file: \(stringsFilePath)") } } } } } else { - print("Please provide the root directory for source code files as a command line argument.") + standardOut.write("Please provide the root directory for source code files as a command line argument.") }