Skip to content

Commit 474fd99

Browse files
Catch implicit positions in .strings files (#48)
* Catch implicit positions in .strings files * Formatting
1 parent 45cb85e commit 474fd99

12 files changed

+231
-67
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ uninstall:
2222
rm -f $(INSTALL_PATH)
2323

2424
format_code:
25-
swiftformat Sources Tests
25+
mint run swiftformat Sources Tests
2626

2727
# Homebrew discourages self-submission unless the project is popular, so this is commented out for now.
2828
# publish: zip_binary bump_brew

Sources/LocheckCommand/main.swift

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ struct XCStrings: HasIgnoreWithShorthand, ParsableCommand {
105105
parseAndValidateXCStrings(
106106
base: try! File(path: base.argument),
107107
translation: translationFile,
108+
baseLanguageName: (try! File(path: base.argument)).nameExcludingExtension,
108109
translationLanguageName: translationFile.nameExcludingExtension,
109110
problemReporter: problemReporter)
110111
}

Sources/LocheckLogic/Problems.swift

+23-2
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,27 @@ struct StringHasMissingArguments: Problem, StringsProblem, Equatable {
192192
var message: String { "'\(key)' does not include argument(s) at \(args.joined(separator: ", "))" }
193193
}
194194

195+
struct StringHasImplicitPosition: Problem, StringsProblem, Equatable {
196+
var kindIdentifier: String { "string_has_implicit_position" }
197+
var uniquifyingInformation: String { "\(language)-\(key)-\(position)" }
198+
var severity: Severity { .warning }
199+
let base: String?
200+
let translation: String?
201+
let key: String
202+
let value: String
203+
let position: Int
204+
let language: String
205+
let suggestion: String
206+
207+
var message: String {
208+
if key == value {
209+
return "Argument \(position) in '\(key)' has an implicit position. Use an explicit position for safety (\(suggestion))."
210+
} else {
211+
return "Argument \(position) in translation of '\(key)' ('\(value)') has an implicit position. Use an explicit position for safety (\(suggestion))."
212+
}
213+
}
214+
}
215+
195216
struct StringArrayItemCountMismatch: Problem, StringsProblem, Equatable {
196217
var base: String?
197218
var translation: String?
@@ -219,14 +240,14 @@ struct StringsdictEntryContainsNoVariablesProblem: Problem, StringsdictProblem,
219240
struct StringsdictEntryHasImplicitPosition: Problem, StringsdictProblem, Equatable {
220241
var kindIdentifier: String { "stringsdict_entry_has_implicit_position" }
221242
var uniquifyingInformation: String { "\(key)-\(position)-\(permutation)" }
222-
// We have code to detect this, but without a way of disabling it per-project yet, it's not reported.
223243
var severity: Severity { .warning }
224244
let key: String
225245
let position: Int
226246
let permutation: String
247+
let suggestion: String
227248

228249
var message: String {
229-
"Argument \(position) in permutation '\(permutation)' of '\(key)' has an implicit position. Use an explicit position for safety."
250+
"Argument \(position) in permutation '\(permutation)' of '\(key)' has an implicit position. Use an explicit position for safety (\(suggestion))."
230251
}
231252
}
232253

Sources/LocheckLogic/Types/FormatArgument.swift

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public struct FormatArgument: Equatable, Hashable {
1414
let specifier: String
1515
let position: Int
1616
let isPositionExplicit: Bool
17+
18+
var asExplicit: String { "%$\(position)\(specifier)" }
1719
}
1820

1921
extension FormatArgument {

Sources/LocheckLogic/Types/LocalizedStringPair.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ extension LocalizedStringPair {
2929
string: String,
3030
path: String,
3131
line: Int,
32+
basePath: String,
33+
baseLineFallback: Int = 0, // Either zero (this is a translation) or a repeat of 'line' (this is the base)
3234
baseStringMap: [String: FormatString]? = nil) { // only pass for translation strings
3335
guard
3436
let match = Expressions.stringPairRegex.lo_matches(in: string).first,
@@ -47,7 +49,7 @@ extension LocalizedStringPair {
4749
if let base = baseStringMap?[key] {
4850
self.base = base
4951
} else {
50-
base = FormatString(string: key, path: path, line: line)
52+
base = FormatString(string: key, path: basePath, line: baseLineFallback)
5153
}
5254
translation = FormatString(string: String(valueSequence), path: path, line: line)
5355
}

Sources/LocheckLogic/Types/StringsdictEntry.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public struct StringsdictEntry: Equatable {
7474
StringsdictEntryHasImplicitPosition(
7575
key: key,
7676
position: arg.position,
77-
permutation: string.string),
77+
permutation: string.string,
78+
suggestion: arg.asExplicit),
7879
line)
7980
}
8081

Sources/LocheckLogic/Validators/parseAndValidateXCStrings.swift

+39-3
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,25 @@ import Foundation
1414
public func parseAndValidateXCStrings(
1515
base: File,
1616
translation: File,
17+
baseLanguageName: String,
1718
translationLanguageName: String,
1819
problemReporter: ProblemReporter) {
19-
let collectLines = { (file: File, baseStringMap: [String: FormatString]) -> [LocalizedStringPair] in
20+
let collectLines = { (file: File, isBase: Bool, baseStringMap: [String: FormatString]) -> [LocalizedStringPair] in
2021
file.lo_getLines(problemReporter: problemReporter)?
2122
.enumerated()
2223
.compactMap {
2324
LocalizedStringPair(
2425
string: $0.1,
2526
path: file.path,
2627
line: $0.0 + 1,
28+
basePath: base.path,
29+
baseLineFallback: isBase ? $0.0 + 1 : 0,
2730
baseStringMap: baseStringMap)
2831
} ?? []
2932
}
3033

3134
// Compare similarly-named files 1-to-1
32-
let baseStrings: [LocalizedStringPair] = collectLines(base, [:])
35+
let baseStrings: [LocalizedStringPair] = collectLines(base, true, [:])
3336
guard !baseStrings.isEmpty else { return }
3437

3538
let baseStringMap = baseStrings.lo_makeDictionary(
@@ -42,10 +45,11 @@ public func parseAndValidateXCStrings(
4245
lineNumber: value.line)
4346
})
4447

45-
let translationStrings = collectLines(translation, baseStringMap)
48+
let translationStrings = collectLines(translation, false, baseStringMap)
4649
validateStrings(
4750
baseStrings: baseStrings,
4851
translationStrings: translationStrings,
52+
baseLanguageName: baseLanguageName,
4953
translationLanguageName: translationLanguageName,
5054
problemReporter: problemReporter)
5155
}
@@ -57,6 +61,7 @@ public func parseAndValidateXCStrings(
5761
func validateStrings(
5862
baseStrings: [LocalizedStringPair],
5963
translationStrings: [LocalizedStringPair],
64+
baseLanguageName: String,
6065
translationLanguageName: String,
6166
problemReporter: ProblemReporter) {
6267
// MARK: Ensure all base strings appear in this translation
@@ -128,7 +133,38 @@ func validateStrings(
128133

129134
let baseArgs = translationString.base.arguments.sorted(by: { $0.position < $1.position })
130135

136+
if baseArgs.count > 1 {
137+
for arg in baseArgs where !arg.isPositionExplicit {
138+
// This might be reported multiple times, but it's deduped
139+
problemReporter.report(
140+
StringHasImplicitPosition(
141+
base: translationString.base.string,
142+
translation: translationString.base.string,
143+
key: translationString.key,
144+
value: translationString.base.string,
145+
position: arg.position,
146+
language: baseLanguageName,
147+
suggestion: arg.asExplicit),
148+
path: translationString.base.path,
149+
lineNumber: translationString.base.line)
150+
}
151+
}
152+
131153
for arg in translationString.translation.arguments {
154+
if !arg.isPositionExplicit && translationString.translation.arguments.count > 1 {
155+
problemReporter.report(
156+
StringHasImplicitPosition(
157+
base: translationString.base.string,
158+
translation: translationString.translation.string,
159+
key: translationString.key,
160+
value: translationString.translation.string,
161+
position: arg.position,
162+
language: translationLanguageName,
163+
suggestion: arg.asExplicit),
164+
path: translationString.path,
165+
lineNumber: translationString.line)
166+
}
167+
132168
guard let baseArg = baseArgs.first(where: { $0.position == arg.position }) else {
133169
continue // we already logged an error for this above
134170
}

Sources/LocheckLogic/Validators/validateLproj.swift

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public func validateLproj(base: LprojFiles, translation: LprojFiles, problemRepo
2323
parseAndValidateXCStrings(
2424
base: file,
2525
translation: counterpart,
26+
baseLanguageName: base.name,
2627
translationLanguageName: translation.name,
2728
problemReporter: problemReporter)
2829
}

0 commit comments

Comments
 (0)