Skip to content

Commit

Permalink
enhanced lint rules
Browse files Browse the repository at this point in the history
code organization
moving towards a more configurable linter
  • Loading branch information
g-Off committed Nov 23, 2018
1 parent b8877c8 commit 4e4bd06
Show file tree
Hide file tree
Showing 22 changed files with 438 additions and 148 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CONFIGURATION = debug
debug: build

release: CONFIGURATION = release
release: SWIFTC_FLAGS += --static-swift-stdlib
release: clean build

build:
Expand Down
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@
"revision": "235aacc514cb81a6881364b0fedcb3dd083228f3",
"version": "0.3.0"
}
},
{
"package": "SwiftyTextTable",
"repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state": {
"branch": null,
"revision": "7b8661865f0d9590a4b7c146237fecd99f3d8406",
"version": "0.8.2"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "26ab35f50ea891e8edefcc9d975db2f6b67e1d68",
"version": "1.0.1"
}
}
]
},
Expand Down
13 changes: 9 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// swift-tools-version:4.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "stringray",
dependencies: [
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.3.0")
.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")
],
targets: [
.target(
name: "stringray",
dependencies: ["Utility"]),
dependencies: [
"Utility",
"Yams",
"SwiftyTextTable"
]
),
.testTarget(
name: "stringrayTests",
dependencies: ["stringray"]),
Expand Down
18 changes: 16 additions & 2 deletions Sources/stringray/Commands/CommandRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ struct CommandRegistry {
private let parser: ArgumentParser
private var commands: [Command] = []

init(usage: String, overview: String) {
parser = ArgumentParser(usage: usage, overview: overview)
private let versionOption: OptionArgument<Bool>?
private let version: Version?

init(usage: String, overview: String, version: Version? = nil) {
self.parser = ArgumentParser(usage: usage, overview: overview)
self.version = version
if version != nil {
self.versionOption = parser.add(option: "--version", shortName: "-v", kind: Bool.self, usage: "Version", completion: nil)
} else {
self.versionOption = nil
}
}

mutating func register(command: Command.Type) {
Expand All @@ -40,6 +49,11 @@ struct CommandRegistry {
}

private func process(arguments: ArgumentParser.Result) throws {
if let versionOption = versionOption, arguments.get(versionOption) == true, let version = version {
stdoutStream.write("\(version)\n")
stdoutStream.flush()
return
}
guard let subparser = arguments.subparser(parser),
let command = commands.first(where: { $0.command == subparser }) else {
parser.printUsage(on: stdoutStream)
Expand Down
64 changes: 47 additions & 17 deletions Sources/stringray/Commands/LintCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,75 @@

import Foundation
import Utility
import Basic
import SwiftyTextTable

struct LintCommand: Command {
private struct Arguments {
var inputFile: Foundation.URL!
var inputFile: [AbsolutePath] = []
var listRules: Bool = false
}

let command: String = "lint"
let overview: String = ""
private var rules: [LintRule] = [
MissingLocalizationLintRule(),
OrphanedLocalizationLintRule()
]
let overview: String = "Checks for warnings or errors on the given strings table."

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 inputFile = subparser.add(option: "--input", shortName: "-i", kind: [PathArgument].self, strategy: .oneByOne, usage: nil, completion: .filename)
binder.bind(option: inputFile) { (arguments, inputFiles) in
arguments.inputFile = inputFiles.map { $0.path }
}

binder.bind(positional: inputFile) { (arguments, inputFile) in
arguments.inputFile = URL(fileURLWithPath: inputFile.path.asString)
let listRules = subparser.add(option: "--list", shortName: "-l", kind: Bool.self, usage: "List available rules and default configuration", completion: .none)
binder.bind(option: listRules) { (arguments, listRules) in
arguments.listRules = listRules
}
}

func run(with arguments: ArgumentParser.Result) throws {
var commandArgs = Arguments()
try binder.fill(parseResult: arguments, into: &commandArgs)
try lint(url: commandArgs.inputFile)
if commandArgs.listRules {
listRules()
} else {
try lint(files: commandArgs.inputFile)
}
}

private func listRules() {
let linter = Linter(excluded: [])
let rules = linter.rules
let columns = [
TextTableColumn(header: "id"),
TextTableColumn(header: "name"),
TextTableColumn(header: "description")
]
var table = TextTable(columns: columns)
rules.forEach {
table.addRow(values:
[
$0.info.identifier,
$0.info.name,
$0.info.description
]
)
}
print(table.render())
}

private func lint(url: Foundation.URL) throws {
let tableForLinting = try StringsTable(url: url)
for rule in rules {
let violations = rule.scan(table: tableForLinting, url: url.resourceDirectory)
for violation in violations {
print(violation.reason)
}
private func lint(files: [AbsolutePath]) throws {
var loader = StringsTableLoader()
loader.options = [.lineNumbers]
let linter = Linter(excluded: [])

try files.forEach {
let url = URL(fileURLWithPath: $0.asString)
let table = try loader.load(url: url)
try linter.report(on: table, url: url)
}
}
}
5 changes: 3 additions & 2 deletions Sources/stringray/Commands/MoveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ struct MoveCommand: Command {
}

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

let filteredTable = fromTable.withKeys(matching: matching)
toTable.addEntries(from: filteredTable)
Expand Down
2 changes: 1 addition & 1 deletion Sources/stringray/Commands/RenameCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct RenameCommand: Command {
}

private func rename(url: Foundation.URL, matching: [Match], replacements replacementStrings: [String]) throws {
var table = try StringsTable(url: url)
var table = try StringsTableLoader().load(url: url)
table.replace(matches: matching, replacements: replacementStrings)
try write(to: url.resourceDirectory, table: table)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/stringray/Commands/SortCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct SortCommand: Command {
}

private func sort(url: Foundation.URL) throws {
var table = try StringsTable(url: url)
var table = try StringsTableLoader().load(url: url)
table.sort()
try write(to: url, table: table)
}
Expand Down
29 changes: 25 additions & 4 deletions Sources/stringray/Lint Rules/LintRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,46 @@
import Foundation

protocol LintRule {
func scan(table: StringsTable, url: URL) -> [LintRuleViolation]
var info: RuleInfo { get }
func scan(table: StringsTable, url: URL) throws -> [LintRuleViolation]
}

enum Severity: String {
struct RuleInfo {
let identifier: String
let name: String
let description: String
}

enum Severity: String, CustomStringConvertible {
case warning
case error

var description: String {
return rawValue
}
}

struct LintRuleViolation {
struct Location {
struct Location: CustomStringConvertible {
let file: URL
let line: Int?

var description: String {
var path = file.lastPathComponent
if let line = line {
path.append(":\(line)")
}
return path
}
}

let locale: Locale
let location: Location
let severity: Severity
let reason: String

public init(location: Location, severity: Severity, reason: String) {
public init(locale: Locale, location: Location, severity: Severity, reason: String) {
self.locale = locale
self.location = location
self.severity = severity
self.reason = reason
Expand Down
68 changes: 68 additions & 0 deletions Sources/stringray/Lint Rules/Linter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// Linter.swift
// stringray
//
// Created by Geoffrey Foster on 2018-11-19.
//

import Foundation
import Yams

struct Linter {
static let fileName = ".stringray.yml"

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

let rules: [LintRule]
let reporter: Reporter

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

init(excluded: Set<String> = []) {
let rules = Linter.rules.filter {
!excluded.contains($0.info.identifier)
}
self.init(rules: rules)
}

init(path: String = Linter.fileName, rootPath: String? = nil) throws {
let rootURL = URL(fileURLWithPath: rootPath ?? FileManager.default.currentDirectoryPath, isDirectory: true)
let fullPathURL = URL(fileURLWithPath: path, relativeTo: rootURL)
let yamlString = try String(contentsOf: fullPathURL, encoding: .utf8)
let dict = try Yams.load(yaml: yamlString) as? [String: Any]
let excluded = dict?["excluded"] as? [String] ?? []
self.init(excluded: Set(excluded))
}

func run(on table: StringsTable, url: URL) throws -> [LintRuleViolation] {
return try rules.flatMap {
try $0.scan(table: table, url: url)
}
}

func report(on table: StringsTable, url: URL) throws {
let violations = try run(on: table, url: url)
var outputStream = LinterOutputStream(fileHandle: FileHandle.standardOutput)
reporter.generateReport(for: violations, to: &outputStream)
}
}

private struct LinterOutputStream: TextOutputStream {
private let fileHandle: FileHandle

init(fileHandle: FileHandle) {
self.fileHandle = fileHandle
}

mutating func write(_ string: String) {
guard let data = string.appending("\n").data(using: .utf8) else { return }
fileHandle.write(data)
}
}
12 changes: 7 additions & 5 deletions Sources/stringray/Lint Rules/MissingLocalizationLintRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import Foundation

struct MissingLocalizationLintRule: LintRule {
func scan(table: StringsTable, url: URL) -> [LintRuleViolation] {
let info: RuleInfo = RuleInfo(identifier: "missing_localization", name: "Missing Localization", description: "")

func scan(table: StringsTable, url: URL) throws -> [LintRuleViolation] {
return scanEntries(table: table, url: url) + scanDictEntries(table: table, url: url)
}

Expand All @@ -22,8 +24,8 @@ struct MissingLocalizationLintRule: LintRule {
for missingEntry in missingEntries {
let file = URL(fileURLWithPath: "\(entry.key).lproj/\(table.name).strings", relativeTo: url)
let location = LintRuleViolation.Location(file: file, line: nil)
let reason = "\(entry.key), \(missingEntry.key)"
let violation = LintRuleViolation(location: location, severity: .warning, reason: reason)
let reason = "Missing \(missingEntry.key)"
let violation = LintRuleViolation(locale: entry.key, location: location, severity: .warning, reason: reason)
violations.append(violation)
}
}
Expand All @@ -40,8 +42,8 @@ struct MissingLocalizationLintRule: LintRule {
for missingDictEntry in missingDictEntries {
let file = URL(fileURLWithPath: "\(dictEntry.key).lproj/\(table.name).stringsdict", relativeTo: url)
let location = LintRuleViolation.Location(file: file, line: nil)
let reason = "\(dictEntry.key), \(missingDictEntry.key)"
let violation = LintRuleViolation(location: location, severity: .warning, reason: reason)
let reason = "Missing \(missingDictEntry.key)"
let violation = LintRuleViolation(locale: dictEntry.key, location: location, severity: .warning, reason: reason)
violations.append(violation)
}
}
Expand Down
Loading

0 comments on commit 4e4bd06

Please sign in to comment.