Skip to content

Commit 647a272

Browse files
committed
keep a cache around and use it if available when linting
speeds up lint operation by avoiding having to re-parse all files
1 parent bf5d0f3 commit 647a272

File tree

10 files changed

+172
-58
lines changed

10 files changed

+172
-58
lines changed

Sources/stringray/Commands/Command+Extensions.swift

Lines changed: 0 additions & 44 deletions
This file was deleted.

Sources/stringray/Commands/CopyCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ struct CopyCommand: Command {
5454

5555
let filteredTable = fromTable.withKeys(matching: matching)
5656
toTable.addEntries(from: filteredTable)
57-
try write(to: to.resourceDirectory, table: toTable)
57+
try loader.write(to: to.resourceDirectory, table: toTable)
5858
}
5959
}

Sources/stringray/Commands/LintCommand.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,20 @@ struct LintCommand: Command {
159159
var loader = StringsTableLoader()
160160
loader.options = [.lineNumbers]
161161
let linter = Linter(reporter: reporter, config: config)
162+
var allError = Linter.Error([])
163+
162164
try inputs.forEach {
163165
print("Linting: \($0.tableName)")
164166
let table = try loader.load(url: $0.resourceURL, name: $0.tableName, base: $0.locale)
165-
try linter.report(on: table, url: $0.resourceURL)
167+
do {
168+
try linter.report(on: table, url: $0.resourceURL)
169+
try loader.writeCache(table: table, baseURL: $0.resourceURL)
170+
} catch let error as Linter.Error {
171+
allError.violations.append(contentsOf: error.violations)
172+
}
173+
}
174+
if !allError.violations.isEmpty {
175+
throw allError
166176
}
167177
}
168178
}

Sources/stringray/Commands/MoveCommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ struct MoveCommand: Command {
5555
let filteredTable = fromTable.withKeys(matching: matching)
5656
toTable.addEntries(from: filteredTable)
5757
fromTable.removeEntries(from: filteredTable)
58-
try write(to: to.resourceDirectory, table: toTable)
59-
try write(to: from.resourceDirectory, table: fromTable)
58+
try loader.write(to: to.resourceDirectory, table: toTable)
59+
try loader.write(to: from.resourceDirectory, table: fromTable)
6060
}
6161
}

Sources/stringray/Commands/RenameCommand.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ struct RenameCommand: Command {
4949
}
5050

5151
private func rename(url: Foundation.URL, matching: [Match], replacements replacementStrings: [String]) throws {
52-
var table = try StringsTableLoader().load(url: url)
52+
let loader = StringsTableLoader()
53+
var table = try loader.load(url: url)
5354
table.replace(matches: matching, replacements: replacementStrings)
54-
try write(to: url.resourceDirectory, table: table)
55+
try loader.write(to: url.resourceDirectory, table: table)
5556
}
5657
}

Sources/stringray/Commands/SortCommand.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ struct SortCommand: Command {
3535
}
3636

3737
private func sort(url: Foundation.URL) throws {
38-
var table = try StringsTableLoader().load(url: url)
38+
let loader = StringsTableLoader()
39+
var table = try loader.load(url: url)
3940
table.sort()
40-
try write(to: url.resourceDirectory, table: table)
41+
try loader.write(to: url.resourceDirectory, table: table)
4142
}
4243
}

Sources/stringray/Lint Rules/Linter.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,17 @@ struct Linter {
5050
MissingPlaceholderLintRule()
5151
]
5252

53-
private enum LintError: Swift.Error {
54-
case violations
53+
struct Error: LocalizedError {
54+
var violations: [LintRuleViolation]
55+
init(_ violations: [LintRuleViolation]) {
56+
self.violations = violations
57+
}
58+
59+
var errorDescription: String? {
60+
let errorCount = violations.filter { $0.severity == .error }.count
61+
let warningCount = violations.filter { $0.severity == .warning }.count
62+
return "Encountered \(errorCount) errors and \(warningCount) warnings."
63+
}
5564
}
5665

5766
let rules: [LintRule]
@@ -89,7 +98,7 @@ struct Linter {
8998
var outputStream = LinterOutputStream(fileHandle: FileHandle.standardOutput)
9099
reporter.generateReport(for: violations, to: &outputStream)
91100
if !violations.isEmpty {
92-
throw LintError.violations
101+
throw Linter.Error(violations)
93102
}
94103
}
95104
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// CachedStringsTable.swift
3+
// stringray
4+
//
5+
// Created by Geoffrey Foster on 2018-12-12.
6+
//
7+
8+
import Foundation
9+
10+
struct CachedStringsTable: Codable {
11+
let stringsTable: StringsTable
12+
let cacheKeys: [String: Date]
13+
14+
enum LocalizationType {
15+
case strings
16+
case stringsdict
17+
}
18+
19+
init(stringsTable: StringsTable, cacheKeys: [String: Date]) {
20+
self.stringsTable = stringsTable
21+
self.cacheKeys = cacheKeys
22+
}
23+
24+
func strings(for locale: Locale) -> OrderedSet<StringsTable.Entry>? {
25+
return stringsTable.entries[locale]
26+
}
27+
28+
func stringsDict(for locale: Locale) -> [String: StringsTable.DictEntry]? {
29+
return stringsTable.dictEntries[locale]
30+
}
31+
32+
func isCacheValid(for locale: Locale, type: LocalizationType, base: URL) -> Bool {
33+
do {
34+
let fileURL: URL
35+
switch type {
36+
case .strings:
37+
fileURL = try base.stringsURL(tableName: stringsTable.name, locale: locale)
38+
case .stringsdict:
39+
fileURL = try base.stringsDictURL(tableName: stringsTable.name, locale: locale)
40+
}
41+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
42+
guard let modificationDate = attributes[.modificationDate] as? Date else { return false }
43+
return modificationDate == cacheKeys[CachedStringsTable.cacheKey(for: locale, type: type)]
44+
} catch {
45+
return false
46+
}
47+
}
48+
49+
static func cacheKey(for locale: Locale, type: LocalizationType) -> String {
50+
switch type {
51+
case .strings:
52+
return "\(locale.identifier).strings"
53+
case .stringsdict:
54+
return "\(locale.identifier).stringsdict"
55+
}
56+
}
57+
}

Sources/stringray/Strings Table/StringsTable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
struct StringsTable: Codable {
11+
struct StringsTable: Codable {
1212
typealias EntriesType = [Locale: OrderedSet<Entry>]
1313
typealias DictEntriesType = [Locale: [String: DictEntry]]
1414

Sources/stringray/Strings Table/StringsTableLoader.swift

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct StringsTableLoader {
2424
public init(rawValue: UInt) { self.rawValue = rawValue }
2525

2626
public static let lineNumbers = Options(rawValue: 1 << 0)
27+
public static let ignoreCached = Options(rawValue: 1 << 1)
2728
}
2829

2930
var options: Options = []
@@ -40,23 +41,95 @@ struct StringsTableLoader {
4041
var entries: StringsTable.EntriesType = [:]
4142
var dictEntries: StringsTable.DictEntriesType = [:]
4243

44+
var cached: CachedStringsTable?
45+
if !options.contains(.ignoreCached), let url = cacheURL(for: name), let data = try? Data(contentsOf: url) {
46+
let decoder = PropertyListDecoder()
47+
cached = try? decoder.decode(CachedStringsTable.self, from: data)
48+
}
49+
4350
try url.lprojURLs.forEach {
4451
guard let locale = $0.locale else { return }
4552

4653
let stringsTableURL = $0.appendingPathComponent(name).appendingPathExtension("strings")
47-
if let reachable = try? stringsTableURL.checkResourceIsReachable(), reachable == true {
54+
if let cached = cached, cached.isCacheValid(for: locale, type: .strings, base: url), let cachedStrings = cached.strings(for: locale) {
55+
entries[locale] = cachedStrings
56+
} else if let reachable = try? stringsTableURL.checkResourceIsReachable(), reachable == true {
4857
entries[locale] = try load(from: stringsTableURL, options: options)
4958
}
5059

5160
let stringsDictTableURL = $0.appendingPathComponent(name).appendingPathExtension("stringsdict")
52-
if let reachable = try? stringsDictTableURL.checkResourceIsReachable(), reachable == true {
61+
if let cached = cached, cached.isCacheValid(for: locale, type: .stringsdict, base: url), let cachedStringsDict = cached.stringsDict(for: locale) {
62+
dictEntries[locale] = cachedStringsDict
63+
} else if let reachable = try? stringsDictTableURL.checkResourceIsReachable(), reachable == true {
5364
dictEntries[locale] = try load(from: stringsDictTableURL)
5465
}
5566
}
5667

5768
return StringsTable(name: name, base: base, entries: entries, dictEntries: dictEntries)
5869
}
5970

71+
func write(to url: Foundation.URL, table: StringsTable) throws {
72+
for (languageId, languageEntries) in table.entries where !languageEntries.isEmpty {
73+
let fileURL = try url.stringsURL(tableName: table.name, locale: languageId)
74+
guard let outputStream = OutputStream(url: fileURL, append: false) else { continue }
75+
outputStream.open()
76+
var firstEntry = true
77+
for entry in languageEntries {
78+
if !firstEntry {
79+
outputStream.write(string: "\n")
80+
}
81+
firstEntry = false
82+
outputStream.write(string: "\(entry)\n")
83+
}
84+
outputStream.close()
85+
}
86+
87+
for (languageId, languageEntries) in table.dictEntries where !languageEntries.isEmpty {
88+
let fileURL = try url.stringsDictURL(tableName: table.name, locale: languageId)
89+
let encoder = PropertyListEncoder()
90+
encoder.outputFormat = .xml
91+
let data = try encoder.encode(languageEntries)
92+
try data.write(to: fileURL, options: [.atomic])
93+
}
94+
}
95+
96+
func writeCache(table: StringsTable, baseURL: URL) throws {
97+
var cacheKeys: [String: Date] = [:]
98+
99+
for (languageId, languageEntries) in table.entries where !languageEntries.isEmpty {
100+
let fileURL = try baseURL.stringsURL(tableName: table.name, locale: languageId)
101+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
102+
guard let modificationDate = attributes[.modificationDate] as? Date else { continue }
103+
cacheKeys[CachedStringsTable.cacheKey(for: languageId, type: .strings)] = modificationDate
104+
}
105+
106+
for (languageId, languageEntries) in table.dictEntries where !languageEntries.isEmpty {
107+
let fileURL = try baseURL.stringsDictURL(tableName: table.name, locale: languageId)
108+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
109+
guard let modificationDate = attributes[.modificationDate] as? Date else { continue }
110+
cacheKeys[CachedStringsTable.cacheKey(for: languageId, type: .stringsdict)] = modificationDate
111+
}
112+
113+
let cachedTable = CachedStringsTable(stringsTable: table, cacheKeys: cacheKeys)
114+
let encoder = PropertyListEncoder()
115+
encoder.outputFormat = .binary
116+
let cachedData = try encoder.encode(cachedTable)
117+
guard let url = cacheURL(for: table.name) else { return }
118+
try cachedData.write(to: url, options: [.atomic])
119+
}
120+
121+
private func cacheURL(for tableName: String) -> Foundation.URL? {
122+
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "net.g-Off.stringray"
123+
let filePath = "\(bundleIdentifier)/\(tableName).localization"
124+
guard let cacheURL = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: filePath), create: true)
125+
else {
126+
return nil
127+
}
128+
let fileURL = URL(fileURLWithPath: filePath, relativeTo: cacheURL)
129+
try! FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
130+
return URL(fileURLWithPath: filePath, relativeTo: cacheURL)
131+
}
132+
60133
private func load(from url: URL, options: Options) throws -> OrderedSet<StringsTable.Entry> {
61134
func lineNumber(scanLocation: Int, newlineLocations: [Int]) -> Int {
62135
var lastIndex = 0
@@ -123,3 +196,10 @@ struct StringsTableLoader {
123196
return try decoder.decode([String: StringsTable.DictEntry].self, from: data)
124197
}
125198
}
199+
200+
private extension OutputStream {
201+
func write(string: String) {
202+
let encodedDataArray = [UInt8](string.utf8)
203+
write(encodedDataArray, maxLength: encodedDataArray.count)
204+
}
205+
}

0 commit comments

Comments
 (0)