Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/.build
/Packages
/*.xcodeproj
.swiftpm
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM swift:4.2.1
COPY Package.swift ./Package.swift
COPY Sources ./Sources
COPY Tests ./Tests
RUN swift test --configuration debug
44 changes: 0 additions & 44 deletions Makefile

This file was deleted.

1 change: 0 additions & 1 deletion Overrides.xcconfig

This file was deleted.

12 changes: 6 additions & 6 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 27 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
// swift-tools-version:4.2
// swift-tools-version:5.0
import PackageDescription

let package = Package(
name: "stringray",
platforms: [
.macOS(.v10_14)
],
products: [
.executable(
name: "stringray",
targets: ["stringray"]
),
.library(
name: "RayGun",
targets: ["RayGun"]
)
],
dependencies: [
.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/g-Off/XcodeProject.git", from: "0.4.0"),
.package(url: "https://github.com/g-Off/CommandRegistry.git", .branch("master"))
.package(url: "https://github.com/g-Off/XcodeProject.git", from: "0.5.0-alpha.3"),
.package(url: "https://github.com/g-Off/CommandRegistry.git", from: "0.1.0"),
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.3.0")
],
targets: [
.target(
name: "stringray",
dependencies: [
"Yams",
"CommandRegistry",
"RayGun",
"SwiftyTextTable",
"XcodeProject",
"CommandRegistry"
"Utility",
"Yams",
]
),
.target(
name: "RayGun",
dependencies: [
]
),
.testTarget(
name: "stringrayTests",
dependencies: ["stringray"]),
dependencies: ["RayGun"]),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import Foundation

extension URL {
var tableName: String? {
extension Foundation.URL {
public var tableName: String? {
var url = self
if ["strings", "stringsdict"].contains(url.pathExtension) {
url.deletePathExtension()
Expand All @@ -17,7 +17,7 @@ extension URL {
return nil
}

var locale: Locale? {
public var locale: Locale? {
var url = self
if ["strings", "stringsdict"].contains(url.pathExtension) {
url.deleteLastPathComponent()
Expand All @@ -29,7 +29,7 @@ extension URL {
return nil
}

var resourceDirectory: URL {
public var resourceDirectory: Foundation.URL {
var dir = self
if dir.pathExtension == "strings" || dir.pathExtension == "stringsdict" {
dir.deleteLastPathComponent()
Expand All @@ -40,38 +40,38 @@ extension URL {
return dir
}

var lprojURLs: [URL] {
var lprojURLs: [Foundation.URL] {
let directories = try? FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil, options: []).filter { (url) -> Bool in
return url.pathExtension == "lproj"
}
return directories ?? []
}

func stringsFiles(tableName: String) -> [URL] {
func stringsFiles(tableName: String) -> [Foundation.URL] {
return files(tableName: tableName, ext: "strings")
}

func stringsDictFiles(tableName: String) -> [URL] {
func stringsDictFiles(tableName: String) -> [Foundation.URL] {
return files(tableName: tableName, ext: "stringsdict")
}

private func files(tableName: String, ext: String) -> [URL] {
private func files(tableName: String, ext: String) -> [Foundation.URL] {
return lprojURLs.compactMap { (lprojURL) in
let url = lprojURL.appendingPathComponent(tableName).appendingPathExtension(ext)
guard let reachable = try? url.checkResourceIsReachable(), reachable == true else { return nil }
return url
}
}

func stringsURL(tableName: String, locale: Locale) throws -> URL {
func stringsURL(tableName: String, locale: Locale) throws -> Foundation.URL {
return try fileURL(tableName: tableName, locale: locale, ext: "strings", create: true)
}

func stringsDictURL(tableName: String, locale: Locale) throws -> URL {
func stringsDictURL(tableName: String, locale: Locale) throws -> Foundation.URL {
return try fileURL(tableName: tableName, locale: locale, ext: "stringsdict", create: true)
}

private func fileURL(tableName: String, locale: Locale, ext: String, create: Bool) throws -> URL {
private func fileURL(tableName: String, locale: Locale, ext: String, create: Bool) throws -> Foundation.URL {
let lprojURL = appendingPathComponent("\(locale.identifier).lproj", isDirectory: true)
if create {
try FileManager.default.createDirectory(at: lprojURL, withIntermediateDirectories: true, attributes: nil)
Expand Down
56 changes: 56 additions & 0 deletions Sources/RayGun/Lint Rules/LintRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// LintRule.swift
// stringray
//
// Created by Geoffrey Foster on 2018-11-07.
//

import Foundation

public protocol LintRule {
var info: RuleInfo { get }
func scan(table: StringsTable, url: Foundation.URL, config: Linter.Config.Rule?) throws -> [LintRuleViolation]
}

public struct RuleInfo {
public let identifier: String
public let name: String
public let description: String
public let severity: Severity
}

public enum Severity: String, CustomStringConvertible, Decodable {
case warning
case error

public var description: String {
return rawValue
}
}

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

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

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

public init(locale: Locale, location: Location, severity: Severity, reason: String) {
self.locale = locale
self.location = location
self.severity = severity
self.reason = reason
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
//

import Foundation
import Yams

struct Linter {
struct Config: Decodable {
struct Rule: Decodable {
public struct Linter {
public struct Config: Decodable {
public struct Rule: Decodable {
let severity: Severity
}
let included: [String]
Expand All @@ -23,57 +22,54 @@ struct Linter {
case rules
}

init() {
public init() {
self.included = []
self.excluded = []
self.rules = [:]
}

init(url: URL) throws {
let string = try String(contentsOf: url, encoding: .utf8)
self = try YAMLDecoder().decode(Config.self, from: string, userInfo: [:])
}

init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.included = try container.decodeIfPresent([String].self, forKey: .included) ?? []
self.excluded = try container.decodeIfPresent([String].self, forKey: .excluded) ?? []
self.rules = try container.decodeIfPresent([String: Rule].self, forKey: .rules) ?? [:]
}
}

static let fileName = ".stringray.yml"
public static let fileName = ".stringray.yml"

static let allRules: [LintRule] = [
public static let allRules: [LintRule] = [
MissingLocalizationLintRule(),
OrphanedLocalizationLintRule(),
MissingPlaceholderLintRule()
MissingPlaceholderLintRule(),
MissingCommentLintRule()
]

struct Error: LocalizedError {
var violations: [LintRuleViolation]
init(_ violations: [LintRuleViolation]) {
public struct Error: LocalizedError {
public private(set) var violations: [LintRuleViolation]

public init(_ violations: [LintRuleViolation]) {
self.violations = violations
}

var errorDescription: String? {
public 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]
public let rules: [LintRule]
private let reporter: Reporter
private let config: Config

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

private func run(on table: StringsTable, url: URL) throws -> [LintRuleViolation] {
private func run(on table: StringsTable, url: Foundation.URL) throws -> [LintRuleViolation] {
var runnableRules = self.rules

let includedRules = Set(config.included)
Expand All @@ -93,7 +89,7 @@ struct Linter {
}
}

func report(on table: StringsTable, url: URL) throws {
public func report(on table: StringsTable, url: Foundation.URL) throws {
let violations = try run(on: table, url: url)
var outputStream = LinterOutputStream(fileHandle: FileHandle.standardOutput)
reporter.generateReport(for: violations, to: &outputStream)
Expand Down
25 changes: 25 additions & 0 deletions Sources/RayGun/Lint Rules/MissingCommentLintRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// MissingCommentLintRule.swift
// RayGun
//
// Created by Geoffrey Foster on 2019-06-02.
//

import Foundation

struct MissingCommentLintRule: LintRule {
let info: RuleInfo = RuleInfo(identifier: "missing_comment", name: "Missing Comment", description: "", severity: .error)

func scan(table: StringsTable, url: Foundation.URL, config: Linter.Config.Rule?) throws -> [LintRuleViolation] {
var violations: [LintRuleViolation] = []
let file = Foundation.URL(fileURLWithPath: "\(table.base.identifier).lproj/\(table.name).strings", relativeTo: url)
for entry in table.baseEntries where entry.comment == nil {
let line = entry.location?.line
let location = LintRuleViolation.Location(file: file, line: line)
let reason = "Mismatched placeholders \(entry.key)"
let violation = LintRuleViolation(locale: table.base, location: location, severity: config?.severity ?? info.severity, reason: reason)
violations.append(violation)
}
return violations
}
}
Loading