diff --git a/CHANGELOG.md b/CHANGELOG.md index 022d602..a2c162d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,9 @@ Due to the removal of legacy code, there are a few breaking changes in this new ### New Features -_None_ +* Throw an error if a format string has mismatching types for the same placeholde position. + [David Jennes](https://github.com/djbe) + [#44](https://github.com/SwiftGen/SwiftGenKit/issues/44) ### Internal Changes diff --git a/Sources/Parsers/StringsFileParser.swift b/Sources/Parsers/StringsFileParser.swift index 98e81a8..c7ceaf7 100644 --- a/Sources/Parsers/StringsFileParser.swift +++ b/Sources/Parsers/StringsFileParser.swift @@ -10,6 +10,7 @@ import PathKit public enum StringsFileParserError: Error, CustomStringConvertible { case failureOnLoading(path: String) case invalidFormat + case invalidPlaceholder(previous: StringsFileParser.PlaceholderType, new: StringsFileParser.PlaceholderType) public var description: String { switch self { @@ -17,6 +18,8 @@ public enum StringsFileParserError: Error, CustomStringConvertible { return "Failed to load a file at \"\(path)\"" case .invalidFormat: return "Invalid strings file" + case .invalidPlaceholder(let previous, let new): + return "Invalid placeholder type \(new) (previous: \(previous))" } } } @@ -44,7 +47,7 @@ public final class StringsFileParser { } for (key, translation) in dict { - addEntry(Entry(key: key, translation: translation)) + addEntry(try Entry(key: key, translation: translation)) } } @@ -81,8 +84,8 @@ public final class StringsFileParser { } } - public static func placeholders(fromFormat str: String) -> [PlaceholderType] { - return StringsFileParser.placeholders(fromFormat: str) + public static func placeholders(fromFormat str: String) throws -> [PlaceholderType] { + return try StringsFileParser.placeholders(fromFormat: str) } } @@ -104,8 +107,8 @@ public final class StringsFileParser { self.init(key: key, translation: translation, types: types) } - public init(key: String, translation: String) { - let types = PlaceholderType.placeholders(fromFormat: translation) + public init(key: String, translation: String) throws { + let types = try PlaceholderType.placeholders(fromFormat: translation) self.init(key: key, translation: translation, types: types) } } @@ -133,7 +136,7 @@ public final class StringsFileParser { }() // "I give %d apples to %@" --> [.Int, .String] - private static func placeholders(fromFormat formatString: String) -> [PlaceholderType] { + private static func placeholders(fromFormat formatString: String) throws -> [PlaceholderType] { let range = NSRange(location: 0, length: (formatString as NSString).length) // Extract the list of chars (conversion specifiers) and their optional positional specifier @@ -178,6 +181,10 @@ public final class StringsFileParser { while list.count <= insertionPos-1 { list.append(.unknown) } + let previous = list[insertionPos-1] + guard previous == .unknown || previous == p else { + throw StringsFileParserError.invalidPlaceholder(previous: previous, new: p) + } list[insertionPos-1] = p } } diff --git a/Tests/SwiftGenKitTests/StringParserTests.swift b/Tests/SwiftGenKitTests/StringParserTests.swift index 30ab713..5eb7a0d 100644 --- a/Tests/SwiftGenKitTests/StringParserTests.swift +++ b/Tests/SwiftGenKitTests/StringParserTests.swift @@ -8,56 +8,72 @@ import XCTest import SwiftGenKit class StringParserTests: XCTestCase { - func testParseStringPlaceholder() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%@") + func testParseStringPlaceholder() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%@") XCTAssertEqual(placeholders, [.object]) } - func testParseFloatPlaceholder() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%f") + func testParseFloatPlaceholder() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%f") XCTAssertEqual(placeholders, [.float]) } - func testParseDoublePlaceholders() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%g-%e") + func testParseDoublePlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%g-%e") XCTAssertEqual(placeholders, [.float, .float]) } - func testParseFloatWithPrecisionPlaceholders() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%1.2f : %.3f : %+3f : %-6.2f") + func testParseFloatWithPrecisionPlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%1.2f : %.3f : %+3f : %-6.2f") XCTAssertEqual(placeholders, [.float, .float, .float, .float]) } - func testParseIntPlaceholders() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%d-%i-%o-%u-%x") + func testParseIntPlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%d-%i-%o-%u-%x") XCTAssertEqual(placeholders, [.int, .int, .int, .int, .int]) } - func testParseCCharAndStringPlaceholders() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%c-%s") + func testParseCCharAndStringPlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%c-%s") XCTAssertEqual(placeholders, [.char, .cString]) } - func testParsePositionalPlaceholders() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%2$d-%4$f-%3$@-%c") + func testParsePositionalPlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%2$d-%4$f-%3$@-%c") XCTAssertEqual(placeholders, [.char, .int, .object, .float]) } - func testParseComplexFormatPlaceholders() { + func testParseComplexFormatPlaceholders() throws { let format = "%2$1.3d - %4$-.7f - %3$@ - %% - %5$+3c - %%" - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: format) + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: format) // positions 2, 4, 3, 5 set to Int, Float, Object, Char, and position 1 not matched, defaulting to Unknown XCTAssertEqual(placeholders, [.unknown, .int, .object, .float, .char]) } - func testParseEvenEscapePercentSign() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%%foo") + func testParseDuplicateFormatPlaceholders() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "Text: %1$@; %1$@.") + XCTAssertEqual(placeholders, [.object]) + } + + func testParseErrorOnTypeMismatch() throws { + do { + _ = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "Text: %1$@; %1$ld.") + XCTFail("Code did parse string successfully while it was expected to fail for bad syntax") + } catch StringsFileParserError.invalidPlaceholder { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + } + + func testParseEvenEscapePercentSign() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%%foo") // Must NOT map to [.float] XCTAssertEqual(placeholders, []) } - func testParseOddEscapePercentSign() { - let placeholders = StringsFileParser.PlaceholderType.placeholders(fromFormat: "%%%foo") + func testParseOddEscapePercentSign() throws { + let placeholders = try StringsFileParser.PlaceholderType.placeholders(fromFormat: "%%%foo") // Should map to [.float] XCTAssertEqual(placeholders, [.float]) }