Skip to content

Commit

Permalink
adding Xcode support for automatic linting
Browse files Browse the repository at this point in the history
adding the copy command
  • Loading branch information
g-Off committed Dec 11, 2018
1 parent 3967331 commit 18d8b68
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 27 deletions.
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.3.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "1.0.1"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.5.0")
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.5.0"),
.package(url: "https://github.com/g-Off/XcodeProject.git", .branch("master"))
],
targets: [
.target(
name: "stringray",
dependencies: [
"Utility",
"Yams",
"SwiftyTextTable"
"SwiftyTextTable",
"XcodeProject"
]
),
.testTarget(
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ stringray lint -i /path/to/original.lproj/Table.strings
stringray lint -l
````
## Installing
If you have Homebrew installed then the tool is available via `brew install g-Off/tools/stringray`
## Building
Use `make` to build or `make xcode` to generate the Xcode project.
3 changes: 3 additions & 0 deletions Sources/stringray/Commands/CommandRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ struct CommandRegistry {
do {
let parsedArguments = try parse()
try process(arguments: parsedArguments)
exit(EXIT_SUCCESS)
}
catch let error as ArgumentParserError {
print(error.description)
exit(EXIT_FAILURE)
}
catch let error {
print(error.localizedDescription)
exit(EXIT_FAILURE)
}
}

Expand Down
58 changes: 58 additions & 0 deletions Sources/stringray/Commands/CopyCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// CopyCommand.swift
// stringray
//
// Created by Geoffrey Foster on 2018-12-05.
//

import Foundation
import Utility

struct CopyCommand: Command {
private struct Arguments {
var inputFile: Foundation.URL!
var outputFile: Foundation.URL!
var matching: [Match] = []
}
let command: String = "copy"
let overview: String = "Copy keys matching the given pattern from one strings table to another."

private let binder: ArgumentBinder<Arguments>

init(parser: ArgumentParser) {
binder = ArgumentBinder<Arguments>()
let subparser = parser.add(subparser: command, overview: overview)

let inputFile = subparser.add(positional: "inputFile", kind: PathArgument.self, optional: false, usage: "", completion: .filename)
let outputFile = subparser.add(positional: "outputFile", kind: PathArgument.self, optional: false, usage: "", completion: .filename)
let prefix = subparser.add(option: "--prefix", shortName: "-p", kind: [String].self, strategy: .oneByOne, usage: "", completion: nil)

binder.bind(positional: inputFile) { (arguments, inputFile) in
arguments.inputFile = URL(fileURLWithPath: inputFile.path.asString)
}
binder.bind(positional: outputFile) { (arguments, outputFile) in
arguments.outputFile = URL(fileURLWithPath: outputFile.path.asString)
}
binder.bind(option: prefix) { (arguments, matching) in
arguments.matching = matching.map {
return .prefix($0)
}
}
}

func run(with arguments: ArgumentParser.Result) throws {
var commandArgs = Arguments()
try binder.fill(parseResult: arguments, into: &commandArgs)
try copy(from: commandArgs.inputFile, to: commandArgs.outputFile, matching: commandArgs.matching)
}

private func copy(from: Foundation.URL, to: Foundation.URL, matching: [Match]) throws {
let loader = StringsTableLoader()
let fromTable = try loader.load(url: from)
var toTable = try loader.load(url: to)

let filteredTable = fromTable.withKeys(matching: matching)
toTable.addEntries(from: filteredTable)
try write(to: to.resourceDirectory, table: toTable)
}
}
97 changes: 80 additions & 17 deletions Sources/stringray/Commands/LintCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ import Foundation
import Utility
import Basic
import SwiftyTextTable
import XcodeProject

struct LintCommand: Command {
private struct Arguments {
var inputFile: [AbsolutePath] = []
var listRules: Bool = false
var configFile: AbsolutePath?
}

private struct LintInput: Hashable {
let resourceURL: Foundation.URL
let tableName: String
let locale: Locale
}

let command: String = "lint"
Expand All @@ -35,6 +43,11 @@ struct LintCommand: Command {
binder.bind(option: listRules) { (arguments, listRules) in
arguments.listRules = listRules
}

let configFileOption = subparser.add(option: "--config", shortName: "-c", kind: PathArgument.self, usage: "Configuration YAML file", completion: .filename)
binder.bind(option: configFileOption) { (arguments, configFile) in
arguments.configFile = configFile.path
}
}

func run(with arguments: ArgumentParser.Result) throws {
Expand All @@ -46,11 +59,70 @@ struct LintCommand: Command {
return
}

let lintInput: [LintInput]
var reporter: Reporter = ConsoleReporter()
if commandArgs.inputFile.isEmpty {
commandArgs.inputFile = [AbsolutePath(FileManager.default.currentDirectoryPath)]
let environment = ProcessInfo.processInfo.environment
if let xcodeInput = try inputsFromXcode(environment: environment) {
lintInput = xcodeInput
reporter = XcodeReporter()
} else if let currentWorkingDirectory = localFileSystem.currentWorkingDirectory {
lintInput = inputs(from: [currentWorkingDirectory])
} else {
lintInput = []
}
} else {
lintInput = inputs(from: commandArgs.inputFile)
}
try lint(files: commandArgs.inputFile)
try lint(inputs: lintInput, reporter: reporter)
}

private func inputs(from files: [AbsolutePath]) -> [LintInput] {
let inputs: [LintInput] = files.filter {
localFileSystem.exists($0) && localFileSystem.isFile($0)
}.map {
URL(fileURLWithPath: $0.asString)
}.compactMap {
guard let tableName = $0.tableName else { return nil }
guard let locale = $0.locale else { return nil }
return LintInput(resourceURL: $0.resourceDirectory, tableName: tableName, locale: locale)
}
return inputs
}

private func inputsFromXcode(environment: [String: String]) throws -> [LintInput]? {
guard let projectPath = environment["PROJECT_FILE_PATH"],
let targetName = environment["TARGETNAME"],
let infoPlistPath = environment["INFOPLIST_FILE"],
let sourceRoot = environment["SOURCE_ROOT"] else {
return nil
}

let projectURL = URL(fileURLWithPath: projectPath)
let sourceRootURL = URL(fileURLWithPath: sourceRoot)
let infoPlistURL = URL(fileURLWithPath: infoPlistPath, relativeTo: sourceRootURL)
let data = try Data(contentsOf: infoPlistURL)
guard let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let base = plist["CFBundleDevelopmentRegion"] as? String else {
return nil
}
let locale = Locale(identifier: base)

let projectFile = try ProjectFile(url: projectURL)
guard let target = projectFile.project.target(named: targetName) else { return nil }
guard let resourcesBuildPhase = target.resourcesBuildPhase else { return nil }
let variantGroups = resourcesBuildPhase.files.compactMap { $0.fileRef as? PBXVariantGroup }.filter {
guard let name = $0.name else { return false }
return name.hasSuffix(".strings") || name.hasSuffix(".stringsdict")
}
let variantGroupsFiles = variantGroups.flatMap {
$0.children.compactMap { $0 as? PBXFileReference }.compactMap { $0.url }.filter { $0.pathExtension == "strings" || $0.pathExtension == "stringsdict" }
}
let allInputs: [LintInput] = variantGroupsFiles.compactMap {
guard let tableName = $0.tableName else { return nil }
return LintInput(resourceURL: $0.resourceDirectory, tableName: tableName, locale: locale)
}
let uniqueInputs = Set(allInputs)
return Array(uniqueInputs)
}

private func listRules() {
Expand All @@ -74,23 +146,14 @@ struct LintCommand: Command {
print(table.render())
}

private func lint(files: [AbsolutePath]) throws {
private func lint(inputs: [LintInput], reporter: Reporter) throws {
var loader = StringsTableLoader()
loader.options = [.lineNumbers]
let linter = Linter(excluded: [])

try files.forEach {
let url = URL(fileURLWithPath: $0.asString)
print("Linting: \(url.path)")

var isDirectory: ObjCBool = false
let fileExists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
if fileExists && !isDirectory.boolValue {
let table = try loader.load(url: url)
try linter.report(on: table, url: url)
} else {
print("Skipping: \(url.path) | this path is a directory or the file does not exist.")
}
let linter = Linter(reporter: reporter)
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)
}
}
}
15 changes: 11 additions & 4 deletions Sources/stringray/Lint Rules/Linter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,26 @@ import Yams
struct Linter {
static let fileName = ".stringray.yml"

private static let rules: [LintRule] = [
static let allRules: [LintRule] = [
MissingLocalizationLintRule(),
OrphanedLocalizationLintRule(),
MissingPlaceholderLintRule()
]

private enum LintError: Swift.Error {
case violations
}

let rules: [LintRule]
let reporter: Reporter

init(rules: [LintRule], reporter: Reporter = ConsoleReporter()) {
init(rules: [LintRule] = Linter.allRules, reporter: Reporter = ConsoleReporter()) {
self.rules = rules
self.reporter = reporter
}

init(excluded: Set<String> = []) {
let rules = Linter.rules.filter {
let rules = Linter.allRules.filter {
!excluded.contains($0.info.identifier)
}
self.init(rules: rules)
Expand All @@ -41,7 +45,7 @@ struct Linter {
self.init(excluded: Set(excluded))
}

func run(on table: StringsTable, url: URL) throws -> [LintRuleViolation] {
private func run(on table: StringsTable, url: URL) throws -> [LintRuleViolation] {
return try rules.flatMap {
try $0.scan(table: table, url: url)
}
Expand All @@ -51,6 +55,9 @@ struct Linter {
let violations = try run(on: table, url: url)
var outputStream = LinterOutputStream(fileHandle: FileHandle.standardOutput)
reporter.generateReport(for: violations, to: &outputStream)
if !violations.isEmpty {
throw LintError.violations
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct MissingLocalizationLintRule: LintRule {
for entry in entries {
let missingEntries = baseEntries.subtracting(entry.value)
for missingEntry in missingEntries {
let file = URL(fileURLWithPath: "\(entry.key).lproj/\(table.name).strings", relativeTo: url)
let file = URL(fileURLWithPath: "\(entry.key.identifier).lproj/\(table.name).strings", relativeTo: url)
let location = LintRuleViolation.Location(file: file, line: nil)
let reason = "Missing \(missingEntry.key)"
let violation = LintRuleViolation(locale: entry.key, location: location, severity: .warning, reason: reason)
Expand All @@ -40,7 +40,7 @@ struct MissingLocalizationLintRule: LintRule {
for dictEntry in dictEntries {
let missingDictEntries = baseDictEntries.filter { !dictEntry.value.keys.contains($0.key) }
for missingDictEntry in missingDictEntries {
let file = URL(fileURLWithPath: "\(dictEntry.key).lproj/\(table.name).stringsdict", relativeTo: url)
let file = URL(fileURLWithPath: "\(dictEntry.key.identifier).lproj/\(table.name).stringsdict", relativeTo: url)
let location = LintRuleViolation.Location(file: file, line: nil)
let reason = "Missing \(missingDictEntry.key)"
let violation = LintRuleViolation(locale: dictEntry.key, location: location, severity: .warning, reason: reason)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct MissingPlaceholderLintRule: LintRule {
try entry.value.forEach {
let placeholder = try PlaceholderType.orderedPlaceholders(from: $0.value)
if let basePlaceholder = placeholders[$0.key], placeholder != basePlaceholder {
let file = URL(fileURLWithPath: "\(entry.key).lproj/\(table.name).strings", relativeTo: url)
let file = URL(fileURLWithPath: "\(entry.key.identifier).lproj/\(table.name).strings", relativeTo: url)
let line = $0.location?.line
let location = LintRuleViolation.Location(file: file, line: line)
let reason = "Mismatched placeholders \($0.key)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct OrphanedLocalizationLintRule: LintRule {
for entry in entries {
let orphanedEntries = entry.value.subtracting(baseEntries)
for orphanedEntry in orphanedEntries {
let file = URL(fileURLWithPath: "\(entry.key).lproj/\(table.name).strings", relativeTo: url)
let file = URL(fileURLWithPath: "\(entry.key.identifier).lproj/\(table.name).strings", relativeTo: url)
guard let line = orphanedEntry.location?.line else { continue }
let location = LintRuleViolation.Location(file: file, line: line)
let reason = "Orphaned \(orphanedEntry.key)"
Expand Down
1 change: 1 addition & 0 deletions Sources/stringray/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Utility

var registry = CommandRegistry(usage: "<command> <options>", overview: "", version: Version(0, 1, 1))
registry.register(command: MoveCommand.self)
registry.register(command: CopyCommand.self)
registry.register(command: SortCommand.self)
registry.register(command: RenameCommand.self)
registry.register(command: LintCommand.self)
Expand Down

0 comments on commit 18d8b68

Please sign in to comment.