Skip to content

Commit

Permalink
keep a cache around and use it if available when linting
Browse files Browse the repository at this point in the history
speeds up lint operation by avoiding having to re-parse all files
  • Loading branch information
g-Off committed Dec 14, 2018
1 parent bf5d0f3 commit 647a272
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 58 deletions.
44 changes: 0 additions & 44 deletions Sources/stringray/Commands/Command+Extensions.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/stringray/Commands/CopyCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ struct CopyCommand: Command {

let filteredTable = fromTable.withKeys(matching: matching)
toTable.addEntries(from: filteredTable)
try write(to: to.resourceDirectory, table: toTable)
try loader.write(to: to.resourceDirectory, table: toTable)
}
}
12 changes: 11 additions & 1 deletion Sources/stringray/Commands/LintCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,20 @@ struct LintCommand: Command {
var loader = StringsTableLoader()
loader.options = [.lineNumbers]
let linter = Linter(reporter: reporter, config: config)
var allError = Linter.Error([])

try inputs.forEach {
print("Linting: \($0.tableName)")
let table = try loader.load(url: $0.resourceURL, name: $0.tableName, base: $0.locale)
try linter.report(on: table, url: $0.resourceURL)
do {
try linter.report(on: table, url: $0.resourceURL)
try loader.writeCache(table: table, baseURL: $0.resourceURL)
} catch let error as Linter.Error {
allError.violations.append(contentsOf: error.violations)
}
}
if !allError.violations.isEmpty {
throw allError
}
}
}
4 changes: 2 additions & 2 deletions Sources/stringray/Commands/MoveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct MoveCommand: Command {
let filteredTable = fromTable.withKeys(matching: matching)
toTable.addEntries(from: filteredTable)
fromTable.removeEntries(from: filteredTable)
try write(to: to.resourceDirectory, table: toTable)
try write(to: from.resourceDirectory, table: fromTable)
try loader.write(to: to.resourceDirectory, table: toTable)
try loader.write(to: from.resourceDirectory, table: fromTable)
}
}
5 changes: 3 additions & 2 deletions Sources/stringray/Commands/RenameCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ struct RenameCommand: Command {
}

private func rename(url: Foundation.URL, matching: [Match], replacements replacementStrings: [String]) throws {
var table = try StringsTableLoader().load(url: url)
let loader = StringsTableLoader()
var table = try loader.load(url: url)
table.replace(matches: matching, replacements: replacementStrings)
try write(to: url.resourceDirectory, table: table)
try loader.write(to: url.resourceDirectory, table: table)
}
}
5 changes: 3 additions & 2 deletions Sources/stringray/Commands/SortCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ struct SortCommand: Command {
}

private func sort(url: Foundation.URL) throws {
var table = try StringsTableLoader().load(url: url)
let loader = StringsTableLoader()
var table = try loader.load(url: url)
table.sort()
try write(to: url.resourceDirectory, table: table)
try loader.write(to: url.resourceDirectory, table: table)
}
}
15 changes: 12 additions & 3 deletions Sources/stringray/Lint Rules/Linter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,17 @@ struct Linter {
MissingPlaceholderLintRule()
]

private enum LintError: Swift.Error {
case violations
struct Error: LocalizedError {
var violations: [LintRuleViolation]
init(_ violations: [LintRuleViolation]) {
self.violations = violations
}

var errorDescription: String? {
let errorCount = violations.filter { $0.severity == .error }.count
let warningCount = violations.filter { $0.severity == .warning }.count
return "Encountered \(errorCount) errors and \(warningCount) warnings."
}
}

let rules: [LintRule]
Expand Down Expand Up @@ -89,7 +98,7 @@ struct Linter {
var outputStream = LinterOutputStream(fileHandle: FileHandle.standardOutput)
reporter.generateReport(for: violations, to: &outputStream)
if !violations.isEmpty {
throw LintError.violations
throw Linter.Error(violations)
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions Sources/stringray/Strings Table/Cache/CachedStringsTable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// CachedStringsTable.swift
// stringray
//
// Created by Geoffrey Foster on 2018-12-12.
//

import Foundation

struct CachedStringsTable: Codable {
let stringsTable: StringsTable
let cacheKeys: [String: Date]

enum LocalizationType {
case strings
case stringsdict
}

init(stringsTable: StringsTable, cacheKeys: [String: Date]) {
self.stringsTable = stringsTable
self.cacheKeys = cacheKeys
}

func strings(for locale: Locale) -> OrderedSet<StringsTable.Entry>? {
return stringsTable.entries[locale]
}

func stringsDict(for locale: Locale) -> [String: StringsTable.DictEntry]? {
return stringsTable.dictEntries[locale]
}

func isCacheValid(for locale: Locale, type: LocalizationType, base: URL) -> Bool {
do {
let fileURL: URL
switch type {
case .strings:
fileURL = try base.stringsURL(tableName: stringsTable.name, locale: locale)
case .stringsdict:
fileURL = try base.stringsDictURL(tableName: stringsTable.name, locale: locale)
}
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
guard let modificationDate = attributes[.modificationDate] as? Date else { return false }
return modificationDate == cacheKeys[CachedStringsTable.cacheKey(for: locale, type: type)]
} catch {
return false
}
}

static func cacheKey(for locale: Locale, type: LocalizationType) -> String {
switch type {
case .strings:
return "\(locale.identifier).strings"
case .stringsdict:
return "\(locale.identifier).stringsdict"
}
}
}
2 changes: 1 addition & 1 deletion Sources/stringray/Strings Table/StringsTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

struct StringsTable: Codable {
struct StringsTable: Codable {
typealias EntriesType = [Locale: OrderedSet<Entry>]
typealias DictEntriesType = [Locale: [String: DictEntry]]

Expand Down
84 changes: 82 additions & 2 deletions Sources/stringray/Strings Table/StringsTableLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct StringsTableLoader {
public init(rawValue: UInt) { self.rawValue = rawValue }

public static let lineNumbers = Options(rawValue: 1 << 0)
public static let ignoreCached = Options(rawValue: 1 << 1)
}

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

var cached: CachedStringsTable?
if !options.contains(.ignoreCached), let url = cacheURL(for: name), let data = try? Data(contentsOf: url) {
let decoder = PropertyListDecoder()
cached = try? decoder.decode(CachedStringsTable.self, from: data)
}

try url.lprojURLs.forEach {
guard let locale = $0.locale else { return }

let stringsTableURL = $0.appendingPathComponent(name).appendingPathExtension("strings")
if let reachable = try? stringsTableURL.checkResourceIsReachable(), reachable == true {
if let cached = cached, cached.isCacheValid(for: locale, type: .strings, base: url), let cachedStrings = cached.strings(for: locale) {
entries[locale] = cachedStrings
} else if let reachable = try? stringsTableURL.checkResourceIsReachable(), reachable == true {
entries[locale] = try load(from: stringsTableURL, options: options)
}

let stringsDictTableURL = $0.appendingPathComponent(name).appendingPathExtension("stringsdict")
if let reachable = try? stringsDictTableURL.checkResourceIsReachable(), reachable == true {
if let cached = cached, cached.isCacheValid(for: locale, type: .stringsdict, base: url), let cachedStringsDict = cached.stringsDict(for: locale) {
dictEntries[locale] = cachedStringsDict
} else if let reachable = try? stringsDictTableURL.checkResourceIsReachable(), reachable == true {
dictEntries[locale] = try load(from: stringsDictTableURL)
}
}

return StringsTable(name: name, base: base, entries: entries, dictEntries: dictEntries)
}

func write(to url: Foundation.URL, table: StringsTable) throws {
for (languageId, languageEntries) in table.entries where !languageEntries.isEmpty {
let fileURL = try url.stringsURL(tableName: table.name, locale: languageId)
guard let outputStream = OutputStream(url: fileURL, append: false) else { continue }
outputStream.open()
var firstEntry = true
for entry in languageEntries {
if !firstEntry {
outputStream.write(string: "\n")
}
firstEntry = false
outputStream.write(string: "\(entry)\n")
}
outputStream.close()
}

for (languageId, languageEntries) in table.dictEntries where !languageEntries.isEmpty {
let fileURL = try url.stringsDictURL(tableName: table.name, locale: languageId)
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let data = try encoder.encode(languageEntries)
try data.write(to: fileURL, options: [.atomic])
}
}

func writeCache(table: StringsTable, baseURL: URL) throws {
var cacheKeys: [String: Date] = [:]

for (languageId, languageEntries) in table.entries where !languageEntries.isEmpty {
let fileURL = try baseURL.stringsURL(tableName: table.name, locale: languageId)
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
guard let modificationDate = attributes[.modificationDate] as? Date else { continue }
cacheKeys[CachedStringsTable.cacheKey(for: languageId, type: .strings)] = modificationDate
}

for (languageId, languageEntries) in table.dictEntries where !languageEntries.isEmpty {
let fileURL = try baseURL.stringsDictURL(tableName: table.name, locale: languageId)
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
guard let modificationDate = attributes[.modificationDate] as? Date else { continue }
cacheKeys[CachedStringsTable.cacheKey(for: languageId, type: .stringsdict)] = modificationDate
}

let cachedTable = CachedStringsTable(stringsTable: table, cacheKeys: cacheKeys)
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let cachedData = try encoder.encode(cachedTable)
guard let url = cacheURL(for: table.name) else { return }
try cachedData.write(to: url, options: [.atomic])
}

private func cacheURL(for tableName: String) -> Foundation.URL? {
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "net.g-Off.stringray"
let filePath = "\(bundleIdentifier)/\(tableName).localization"
guard let cacheURL = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: filePath), create: true)
else {
return nil
}
let fileURL = URL(fileURLWithPath: filePath, relativeTo: cacheURL)
try! FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
return URL(fileURLWithPath: filePath, relativeTo: cacheURL)
}

private func load(from url: URL, options: Options) throws -> OrderedSet<StringsTable.Entry> {
func lineNumber(scanLocation: Int, newlineLocations: [Int]) -> Int {
var lastIndex = 0
Expand Down Expand Up @@ -123,3 +196,10 @@ struct StringsTableLoader {
return try decoder.decode([String: StringsTable.DictEntry].self, from: data)
}
}

private extension OutputStream {
func write(string: String) {
let encodedDataArray = [UInt8](string.utf8)
write(encodedDataArray, maxLength: encodedDataArray.count)
}
}

0 comments on commit 647a272

Please sign in to comment.