Compare commits

...

13 Commits

Author SHA1 Message Date
JP Simard 31fb736284
Fix rebase 2020-12-11 15:12:24 -05:00
JP Simard 00f6a76050
Try more locations if the first doesn't work 2020-12-11 15:09:29 -05:00
JP Simard dcf67cac3c
Clean up 2020-12-11 15:09:28 -05:00
JP Simard e207dcc16f
fixup! Use index for unused imports 2020-12-11 15:09:28 -05:00
JP Simard 0e6dfb9da2
Use index for unused imports 2020-12-11 15:09:27 -05:00
JP Simard 0bec408655
Make `--strict` mutually exclusive with `--lenient` 2020-12-11 14:00:19 -05:00
JP Simard 9adb2deac2
Fix paths argument handling 2020-12-11 13:52:56 -05:00
JP Simard 1aec09282d
fixup! Remove unused imports 2020-12-11 13:02:05 -05:00
JP Simard d781a1261e
Remove unused imports 2020-12-11 12:54:36 -05:00
JP Simard 783976aa52
Add changelog entry 2020-12-11 12:22:55 -05:00
JP Simard bb2af8c5b3
Migrate from Commandant to swift-argument-parser 2020-12-11 12:22:54 -05:00
JP Simard 3a39d8bb49
Rename command files 2020-12-11 12:22:28 -05:00
JP Simard 88283dc547
Update SourceKittenFramework to 0.31.0 2020-12-11 12:22:15 -05:00
35 changed files with 685 additions and 846 deletions

View File

@ -36,7 +36,6 @@ opt_in_rules:
- last_where
- legacy_multiple
- legacy_random
- let_var_whitespace
- literal_expression_end_indentation
- lower_acl_than_parent
- modifier_order

View File

@ -39,6 +39,19 @@
[Bryan Ricker](https://github.com/bricker)
[#3426](https://github.com/realm/SwiftLint/pull/3426)
* The command line syntax has slightly changed due to migrating from the
Commandant command line parsing library to swift-argument-parser.
For the most part the breaking changes are all to make the syntax more
unix compliant and intuitive to use. For example, commands such as
`swiftlint --help` or `swiftlint -h` now work as expected.
The help output from various commands has greatly improved as well.
Notably: `swiftlint autocorrect` was removed in favor of
`swiftlint --fix`.
Previous commands should continue to work temporarily to help with the
transition. Please let us know if there's a command that no longer
works and we'll attempt to add a bridge to help with its transition.
[JP Simard](https://github.com/jpsim)
#### Experimental
* None.
@ -250,6 +263,9 @@
enable it to detect more occurrences of unused declarations.
[JP Simard](https://github.com/jpsim)
* Make the `unused_import` rule run about 2 times faster.
[JP Simard](https://github.com/jpsim)
* Remove unneeded internal locking overhead, leading to increased
performance in multithreaded operations.
[JP Simard](https://github.com/jpsim)

View File

@ -1,40 +1,22 @@
{
"object": {
"pins": [
{
"package": "Commandant",
"repositoryURL": "https://github.com/Carthage/Commandant.git",
"state": {
"branch": null,
"revision": "ab68611013dec67413628ac87c1f29e8427bc8e4",
"version": "0.17.0"
}
},
{
"package": "Nimble",
"repositoryURL": "https://github.com/Quick/Nimble.git",
"state": {
"branch": null,
"revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008",
"version": "8.1.2"
}
},
{
"package": "Quick",
"repositoryURL": "https://github.com/Quick/Quick.git",
"state": {
"branch": null,
"revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792",
"version": "2.2.1"
}
},
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"state": {
"branch": null,
"revision": "c0f960f72fa1e6151695074ffa696e4da6c45ce8",
"version": "0.30.1"
"revision": "7f4be006fe73211b0fd9666c73dc2f2303ffa756",
"version": "0.31.0"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
}
},
{

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.2
import PackageDescription
#if canImport(CommonCrypto)
@ -14,8 +14,8 @@ let package = Package(
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
],
dependencies: [
.package(url: "https://github.com/Carthage/Commandant.git", .upToNextMinor(from: "0.17.0")),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.30.1")),
.package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.3.1")),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.31.0")),
.package(url: "https://github.com/jpsim/Yams.git", from: "4.0.2"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.3.2"))] : []),
@ -23,7 +23,7 @@ let package = Package(
.target(
name: "swiftlint",
dependencies: [
"Commandant",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SwiftLintFramework",
"SwiftyTextTable",
]
@ -31,7 +31,7 @@ let package = Package(
.target(
name: "SwiftLintFramework",
dependencies: [
"SourceKittenFramework",
.product(name: "SourceKittenFramework", package: "SourceKitten"),
"Yams",
] + (addCryptoSwift ? ["CryptoSwift"] : [])
),

View File

@ -30,6 +30,22 @@ extension Array where Element: Hashable {
}
}
extension Array {
func unique<T: Hashable>(by transform: ((Element) -> T)) -> [Element] {
var set = Set<T>()
var result = [Element]()
for value in self {
let transformed = transform(value)
if !set.contains(transformed) {
set.insert(transformed)
result.append(value)
}
}
return result
}
}
extension Array {
static func array(of obj: Any?) -> [Element]? {
if let array = obj as? [Element] {

View File

@ -133,6 +133,22 @@ public struct SourceKittenDictionary {
return value["key.column"] as? Int64
}
/// The USR for this declaration.
var usr: String? {
return value["key.usr"] as? String
}
/// The annotated declaration.
var annotatedDeclaration: String? {
return value["key.annotated_decl"] as? String
}
// The dependencies returned in an index response.
var dependencies: [SourceKittenDictionary]? {
return (value["key.dependencies"] as? [[String: SourceKitRepresentable]])?
.map(SourceKittenDictionary.init)
}
/// The `SwiftDeclarationAttributeKind` values associated with this dictionary.
var enclosedSwiftAttributes: [SwiftDeclarationAttributeKind] {
return swiftAttributes.compactMap { $0.attribute }

View File

@ -118,3 +118,16 @@ extension String {
}
}
}
extension StringView {
/// Converts a line and column pair to a ByteCount offset into the string.
///
/// - parameter line: The 1-indexed line number into the string.
/// - parameter column: The 1-indexed column number into the string.
///
/// - returns: The `ByteCount` offset in bytes into the string.
func byteOffset(forLine line: Int, column: Int) -> ByteCount {
guard line > 0 else { return ByteCount(column - 1) }
return lines[line - 1].byteRange.location + ByteCount(column - 1)
}
}

View File

@ -19,22 +19,4 @@ extension SyntaxKind {
.docComment, .docCommentField, .identifier, .keyword, .number,
.objectLiteral, .parameter, .placeholder, .string,
.stringInterpolationAnchor, .typeidentifier]
/// Syntax kinds that don't have associated module info when getting their cursor info.
static var kindsWithoutModuleInfo: Set<SyntaxKind> {
return [
.attributeBuiltin,
.keyword,
.number,
.docComment,
.string,
.stringInterpolationAnchor,
.attributeID,
.buildconfigKeyword,
.buildconfigID,
.commentURL,
.comment,
.docCommentField
]
}
}

View File

@ -75,6 +75,20 @@ public final class SwiftLintFile {
return containsAttributesRequiringFoundation(dict: structureDictionary)
}
/// Returns the SourceKit index for the current file, obtained with the specified compiler arguments.
///
/// - parameter compilerArguments: The swiftc arguments needed to compile the file.
///
/// - returns: A `SourceKittenDictionary` with the index response.
func index(compilerArguments: [String]) -> SourceKittenDictionary? {
return path
.flatMap { path in
try? Request.index(file: path, arguments: compilerArguments)
.sendIfNotDisabled()
}
.map(SourceKittenDictionary.init)
}
}
// MARK: - Hashable Conformance

View File

@ -91,15 +91,6 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide
// MARK: - File Extensions
private extension SwiftLintFile {
func index(compilerArguments: [String]) -> SourceKittenDictionary? {
return path
.flatMap { path in
try? Request.index(file: path, arguments: compilerArguments)
.send()
}
.map(SourceKittenDictionary.init)
}
func referencedUSRs(index: SourceKittenDictionary) -> Set<String> {
return Set(index.traverseEntities { entity -> String? in
if let usr = entity.usr,
@ -224,14 +215,6 @@ private extension SwiftLintFile {
}
private extension SourceKittenDictionary {
var usr: String? {
return value["key.usr"] as? String
}
var annotatedDeclaration: String? {
return value["key.annotated_decl"] as? String
}
var isImplicit: Bool {
return value["key.is_implicit"] as? Bool == true
}
@ -321,10 +304,3 @@ private extension SourceKittenDictionary {
}
}
}
private extension StringView {
func byteOffset(forLine line: Int, column: Int) -> ByteCount {
guard line > 0 else { return ByteCount(column - 1) }
return lines[line - 1].byteRange.location + ByteCount(column - 1)
}
}

View File

@ -98,7 +98,12 @@ public struct UnusedImportRule: CorrectableRule, ConfigurationProviderRule, Anal
private extension SwiftLintFile {
func getImportUsage(compilerArguments: [String], configuration: UnusedImportConfiguration) -> [ImportUsage] {
var (imports, usrFragments) = getImportsAndUSRFragments(compilerArguments: compilerArguments)
guard let index = index(compilerArguments: compilerArguments) else {
queuedPrintError("Could not get index")
return []
}
var (imports, usrFragments) = getImportsAndUSRFragments(index: index, compilerArguments: compilerArguments)
// Always disallow 'import Swift' because it's available without importing.
usrFragments.remove("Swift")
@ -108,15 +113,6 @@ private extension SwiftLintFile {
unusedImports.remove("Foundation")
}
if unusedImports.isNotEmpty {
unusedImports.subtract(
operatorImports(
arguments: compilerArguments,
processedTokenOffsets: Set(syntaxMap.tokens.map { $0.offset })
)
)
}
let contentsNSString = stringView.nsString
let unusedImportUsages = rangedAndSortedUnusedImports(of: Array(unusedImports), contents: contentsNSString)
.map { ImportUsage.unused(module: $0, range: $1) }
@ -143,42 +139,88 @@ private extension SwiftLintFile {
return unusedImportUsages + missingImports.sorted().map { .missing(module: $0) }
}
func getImportsAndUSRFragments(compilerArguments: [String]) -> (imports: Set<String>, usrFragments: Set<String>) {
var imports = Set<String>()
func getImportsAndUSRFragments(index: SourceKittenDictionary, compilerArguments: [String])
-> (imports: Set<String>, usrFragments: Set<String>) {
var usrFragments = Set<String>()
var nextIsModuleImport = false
for token in syntaxMap.tokens {
guard let tokenKind = token.kind else {
continue
let allEntities = flatEntities(entity: index)
let referenceEntities = allEntities.filter { entity in
entity.kind?.starts(with: "source.lang.swift.ref") == true &&
entity.kind != "source.lang.swift.ref.module"
}
// swiftlint:disable:next nesting - We really shouldn't trigger when these nested types are in functions
struct Reference {
let line, column: Int
let usr: String
}
let dedupedReferences = referenceEntities
.compactMap { entity in
entity.line.flatMap { line in
entity.column.flatMap { column in
entity.usr.map { usr in
Reference(line: Int(line), column: Int(column), usr: usr)
}
}
}
}
if tokenKind == .keyword, contents(for: token) == "import" {
nextIsModuleImport = true
continue
}
if SyntaxKind.kindsWithoutModuleInfo.contains(tokenKind) {
continue
}
let cursorInfoRequest = Request.cursorInfo(file: path!, offset: token.offset,
arguments: compilerArguments)
// don't cursor-info the same USR at different locations
.unique(by: { $0.usr })
var seenUSRs = Set<String>()
for reference in dedupedReferences {
let nameOffset = stringView.byteOffset(forLine: reference.line, column: reference.column)
let usr = reference.usr
let cursorInfoRequest = Request.cursorInfo(file: path!, offset: nameOffset, arguments: compilerArguments)
guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()).map(SourceKittenDictionary.init) else {
queuedPrintError("Could not get cursor info")
continue
}
if nextIsModuleImport {
if let importedModule = cursorInfo.moduleName,
cursorInfo.kind == "source.lang.swift.ref.module" {
imports.insert(importedModule)
nextIsModuleImport = false
if cursorInfo.usr == usr {
seenUSRs.insert(usr)
if let rootModuleName = cursorInfo.moduleName?.split(separator: ".").first.map(String.init) {
usrFragments.insert(rootModuleName)
}
} else {
let tokens = syntaxMap.tokens
guard let firstTokenIndexAfterNameOffset = tokens.firstIndex(where: { $0.offset > nameOffset }) else {
queuedPrintError("Could not get tokens")
continue
} else {
nextIsModuleImport = false
}
var extraTokensToCheck = 3
let tokensAfterNameOffset = tokens.suffix(from: firstTokenIndexAfterNameOffset)
for token in tokensAfterNameOffset {
extraTokensToCheck -= 1
if extraTokensToCheck == 0 {
break
}
let tokenCursorInfoRequest = Request.cursorInfo(file: path!, offset: token.offset,
arguments: compilerArguments)
let tokenCursorInfo = (try? tokenCursorInfoRequest.sendIfNotDisabled())
.map(SourceKittenDictionary.init)
if tokenCursorInfo?.usr == usr {
seenUSRs.insert(usr)
if let rootModuleName = tokenCursorInfo?.moduleName?.split(separator: ".").first.map(String.init) {
usrFragments.insert(rootModuleName)
break
}
}
}
}
appendUsedImports(cursorInfo: cursorInfo, usrFragments: &usrFragments)
}
return (imports: imports, usrFragments: usrFragments)
let imports = index.dependencies?
.filter { $0.kind?.starts(with: "source.lang.swift.import.module") == true }
.compactMap { $0.name }
.filter { $0 != "Swift" }
return (imports: Set(imports ?? []), usrFragments: usrFragments)
}
func rangedAndSortedUnusedImports(of unusedImports: [String], contents: NSString) -> [(String, NSRange)] {
@ -189,42 +231,6 @@ private extension SwiftLintFile {
.sorted(by: { $0.1.location < $1.1.location })
}
// Operators are omitted in the editor.open request and thus have to be looked up by the indexsource request
func operatorImports(arguments: [String], processedTokenOffsets: Set<ByteCount>) -> Set<String> {
guard let index = (try? Request.index(file: path!, arguments: arguments).sendIfNotDisabled())
.map(SourceKittenDictionary.init) else {
queuedPrintError("Could not get index")
return []
}
let operatorEntities = flatEntities(entity: index).filter { mightBeOperator(kind: $0.kind) }
let offsetPerLine = self.offsetPerLine()
var imports = Set<String>()
for entity in operatorEntities {
if
let line = entity.line,
let column = entity.column,
let lineOffset = offsetPerLine[Int(line) - 1] {
let offset = lineOffset + column - 1
// Filter already processed tokens such as static methods that are not operators
guard !processedTokenOffsets.contains(ByteCount(offset)) else { continue }
let cursorInfoRequest = Request.cursorInfo(file: path!, offset: ByteCount(offset), arguments: arguments)
guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled())
.map(SourceKittenDictionary.init) else {
queuedPrintError("Could not get cursor info")
continue
}
appendUsedImports(cursorInfo: cursorInfo, usrFragments: &imports)
}
}
return imports
}
func flatEntities(entity: SourceKittenDictionary) -> [SourceKittenDictionary] {
let entities = entity.entities
if entities.isEmpty {
@ -233,34 +239,4 @@ private extension SwiftLintFile {
return [entity] + entities.flatMap { flatEntities(entity: $0) }
}
}
func offsetPerLine() -> [Int: Int64] {
return Dictionary(
uniqueKeysWithValues: contents.bridge()
.components(separatedBy: "\n")
.map { Int64($0.bridge().lengthOfBytes(using: .utf8)) }
.reduce(into: [0]) { result, length in
let newLineCharacterLength = Int64(1)
let lineLength = length + newLineCharacterLength
result.append(contentsOf: [(result.last ?? 0) + lineLength])
}
.enumerated()
.map { ($0.offset, $0.element) }
)
}
// Operators that are a part of some body are reported as method.static
func mightBeOperator(kind: String?) -> Bool {
guard let kind = kind else { return false }
return [
"source.lang.swift.ref.function.operator",
"source.lang.swift.ref.function.method.static"
].contains { kind.hasPrefix($0) }
}
func appendUsedImports(cursorInfo: SourceKittenDictionary, usrFragments: inout Set<String>) {
if let rootModuleName = cursorInfo.moduleName?.split(separator: ".").first.map(String.init) {
usrFragments.insert(rootModuleName)
}
}
}

View File

@ -261,11 +261,6 @@ private extension SwiftLintFile {
}
private extension StringView {
func byteOffset(forLine line: Int, column: Int) -> ByteCount {
guard line > 0 else { return ByteCount(column - 1) }
return lines[line - 1].byteRange.location + ByteCount(column - 1)
}
func recursiveByteOffsets(_ dict: [String: Any]) -> [ByteCount] {
let cur: [ByteCount]
if let line = dict["key.line"] as? Int64,

View File

@ -0,0 +1,59 @@
import ArgumentParser
extension SwiftLint {
struct Analyze: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Run analysis rules")
@OptionGroup
var common: LintOrAnalyzeArguments
@Option(help: pathOptionDescription(for: .analyze))
var path: String?
@Flag(help: quietOptionDescription(for: .analyze))
var quiet = false
@Option(help: "The path of the full xcodebuild log to use when running AnalyzerRules.")
var compilerLogPath: String?
@Option(help: "The path of a compilation database to use when running AnalyzerRules.")
var compileCommands: String?
@Argument(help: pathsArgumentDescription(for: .analyze))
var paths = [String]()
mutating func run() throws {
let allPaths: [String]
if let path = path {
allPaths = [path]
} else if !paths.isEmpty {
allPaths = paths
} else {
allPaths = [""] // Analyze files in current working directory if no paths were specified.
}
let options = LintOrAnalyzeOptions(
mode: .analyze,
paths: allPaths,
useSTDIN: false,
configurationFiles: common.config,
strict: common.leniency == .strict,
lenient: common.leniency == .lenient,
forceExclude: common.forceExclude,
useExcludingByPrefix: common.useAlternativeExcluding,
useScriptInputFiles: common.useScriptInputFiles,
benchmark: common.benchmark,
reporter: common.reporter,
quiet: quiet,
cachePath: nil,
ignoreCache: true,
enableAllRules: false,
autocorrect: common.fix,
compilerLogPath: compilerLogPath,
compileCommands: compileCommands
)
let result = LintOrAnalyzeCommand.run(options)
switch result {
case .success:
return
case .failure(let error):
throw error
}
}
}
}

View File

@ -1,98 +0,0 @@
import Commandant
import SwiftLintFramework
struct AnalyzeCommand: CommandProtocol {
let verb = "analyze"
let function = "[Experimental] Run analysis rules"
func run(_ options: AnalyzeOptions) -> Result<(), CommandantError<()>> {
let options = LintOrAnalyzeOptions(options)
if options.autocorrect {
return autocorrect(options)
} else {
return LintOrAnalyzeCommand.run(options)
}
}
private func autocorrect(_ options: LintOrAnalyzeOptions) -> Result<(), CommandantError<()>> {
let storage = RuleStorage()
let configuration = Configuration(options: options)
return configuration.visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
let corrections = linter.correct(using: storage)
if !corrections.isEmpty && !options.quiet {
let correctionLogs = corrections.map({ $0.consoleDescription })
queuedPrint(correctionLogs.joined(separator: "\n"))
}
}.flatMap { files in
if !options.quiet {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
}
return .success(())
}
}
}
struct AnalyzeOptions: OptionsProtocol {
let paths: [String]
let configurationFiles: [String]
let strict: Bool
let lenient: Bool
let forceExclude: Bool
let excludeByPrefix: Bool
let useScriptInputFiles: Bool
let benchmark: Bool
let reporter: String
let quiet: Bool
let enableAllRules: Bool
let autocorrect: Bool
let compilerLogPath: String
let compileCommands: String
// swiftlint:disable line_length
static func create(_ path: String) -> (_ configurationFiles: [String]) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ excludeByPrefix: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ enableAllRules: Bool) -> (_ autocorrect: Bool) -> (_ compilerLogPath: String) -> (_ compileCommands: String) -> (_ paths: [String]) -> AnalyzeOptions {
return { configurationFiles in { strict in { lenient in { forceExclude in { excludeByPrefix in { useScriptInputFiles in { benchmark in { reporter in { quiet in { enableAllRules in { autocorrect in { compilerLogPath in { compileCommands in { paths in
let allPaths: [String]
if !path.isEmpty {
allPaths = [path]
} else {
allPaths = paths
}
return self.init(paths: allPaths, configurationFiles: configurationFiles, strict: strict, lenient: lenient, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, enableAllRules: enableAllRules, autocorrect: autocorrect, compilerLogPath: compilerLogPath, compileCommands: compileCommands)
// swiftlint:enable line_length
}}}}}}}}}}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<AnalyzeOptions, CommandantError<CommandantError<()>>> {
return create
<*> mode <| pathOption(action: "analyze")
<*> mode <| configOption
<*> mode <| Option(key: "strict", defaultValue: false,
usage: "upgrades warnings to serious violations (errors)")
<*> mode <| Option(key: "lenient", defaultValue: false,
usage: "downgrades serious violations to warnings, warning threshold is disabled")
<*> mode <| Option(key: "force-exclude", defaultValue: false,
usage: "exclude files in config `excluded` even if their paths are explicitly specified")
<*> mode <| useAlternativeExcludingOption
<*> mode <| useScriptInputFilesOption
<*> mode <| Option(key: "benchmark", defaultValue: false,
usage: "save benchmarks to benchmark_files.txt " +
"and benchmark_rules.txt")
<*> mode <| Option(key: "reporter", defaultValue: "",
usage: "the reporter used to log errors and warnings")
<*> mode <| quietOption(action: "linting")
<*> mode <| Option(key: "enable-all-rules", defaultValue: false,
usage: "run all rules, even opt-in and disabled ones, ignoring `only_rules`")
<*> mode <| Option(key: "autocorrect", defaultValue: false,
usage: "correct violations whenever possible")
<*> mode <| Option(key: "compiler-log-path", defaultValue: "",
usage: "the path of the full xcodebuild log to use when linting AnalyzerRules")
<*> mode <| Option(key: "compile-commands", defaultValue: "",
usage: "the path of a compilation database to use when linting AnalyzerRules")
// This should go last to avoid eating other args
<*> mode <| pathsArgument(action: "analyze")
}
}

View File

@ -1,91 +0,0 @@
import Commandant
import SwiftLintFramework
struct AutoCorrectCommand: CommandProtocol {
let verb = "autocorrect"
let function = "Automatically correct warnings and errors"
func run(_ options: AutoCorrectOptions) -> Result<(), CommandantError<()>> {
let configuration = Configuration(options: options)
let storage = RuleStorage()
let visitor = options.visitor(with: configuration, storage: storage)
return configuration.visitLintableFiles(with: visitor, storage: storage).flatMap { files in
if !options.quiet {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
}
return .success(())
}
}
}
struct AutoCorrectOptions: OptionsProtocol {
let paths: [String]
let configurationFiles: [String]
let useScriptInputFiles: Bool
let quiet: Bool
let forceExclude: Bool
let excludeByPrefix: Bool
let format: Bool
let cachePath: String
let ignoreCache: Bool
// swiftlint:disable line_length
static func create(_ path: String) -> (_ configurationFiles: [String]) -> (_ useScriptInputFiles: Bool) -> (_ quiet: Bool) -> (_ forceExclude: Bool) ->
(_ excludeByPrefix: Bool) -> (_ format: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ paths: [String]) -> AutoCorrectOptions {
return { configurationFiles in { useScriptInputFiles in { quiet in { forceExclude in { excludeByPrefix in { format in { cachePath in { ignoreCache in { paths in
let allPaths: [String]
if !path.isEmpty {
allPaths = [path]
} else {
allPaths = paths
}
return self.init(paths: allPaths, configurationFiles: configurationFiles, useScriptInputFiles: useScriptInputFiles, quiet: quiet, forceExclude: forceExclude,
excludeByPrefix: excludeByPrefix, format: format, cachePath: cachePath, ignoreCache: ignoreCache)
// swiftlint:enable line_length
}}}}}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<AutoCorrectOptions, CommandantError<CommandantError<()>>> {
return create
<*> mode <| pathOption(action: "correct")
<*> mode <| configOption
<*> mode <| useScriptInputFilesOption
<*> mode <| quietOption(action: "correcting")
<*> mode <| Option(key: "force-exclude", defaultValue: false,
usage: "exclude files in config `excluded` even if their paths are explicitly specified")
<*> mode <| useAlternativeExcludingOption
<*> mode <| Option(key: "format", defaultValue: false,
usage: "should reformat the Swift files")
<*> mode <| Option(key: "cache-path", defaultValue: "",
usage: "the directory of the cache used when correcting")
<*> mode <| Option(key: "no-cache", defaultValue: false,
usage: "ignore cache when correcting")
// This should go last to avoid eating other args
<*> mode <| pathsArgument(action: "correct")
}
fileprivate func visitor(with configuration: Configuration, storage: RuleStorage) -> LintableFilesVisitor {
let cache = ignoreCache ? nil : LinterCache(configuration: configuration)
return LintableFilesVisitor(paths: paths, action: "Correcting", useSTDIN: false, quiet: quiet,
useScriptInputFiles: useScriptInputFiles, forceExclude: forceExclude,
useExcludingByPrefix: excludeByPrefix, cache: cache, parallel: true,
allowZeroLintableFiles: configuration.allowZeroLintableFiles) { linter in
if self.format {
switch configuration.indentation {
case .tabs:
linter.format(useTabs: true, indentWidth: 4)
case .spaces(let count):
linter.format(useTabs: false, indentWidth: count)
}
}
let corrections = linter.correct(using: storage)
if !corrections.isEmpty && !self.quiet {
let correctionLogs = corrections.map({ $0.consoleDescription })
queuedPrint(correctionLogs.joined(separator: "\n"))
}
}
}
}

View File

@ -0,0 +1,29 @@
import ArgumentParser
import Foundation
extension SwiftLint {
struct Docs: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open SwiftLint documentation website in the default web browser"
)
mutating func run() throws {
open(URL(string: "https://realm.github.io/SwiftLint")!)
}
}
}
private func open(_ url: URL) {
let process = Process()
#if os(Linux)
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
let command = "xdg-open"
process.arguments = [command, url.absoluteString]
try? process.run()
#else
process.launchPath = "/usr/bin/env"
let command = "open"
process.arguments = [command, url.absoluteString]
process.launch()
#endif
}

View File

@ -0,0 +1,17 @@
import ArgumentParser
import Foundation
import SwiftLintFramework
extension SwiftLint {
struct GenerateDocs: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Generates markdown documentation for all rules")
@Option(help: "The directory where the documentation should be saved")
var path = "rule_docs"
mutating func run() throws {
try RuleListDocumentation(primaryRuleList)
.write(to: URL(fileURLWithPath: path))
}
}
}

View File

@ -1,33 +0,0 @@
import Commandant
import Foundation
import SwiftLintFramework
struct GenerateDocsCommand: CommandProtocol {
let verb = "generate-docs"
let function = "Generates markdown documentation for all rules"
func run(_ options: GenerateDocsOptions) -> Result<(), CommandantError<()>> {
let docs = RuleListDocumentation(primaryRuleList)
do {
try docs.write(to: URL(fileURLWithPath: options.path))
} catch {
return .failure(.usageError(description: error.localizedDescription))
}
return .success(())
}
}
struct GenerateDocsOptions: OptionsProtocol {
let path: String
static func create(_ path: String) -> GenerateDocsOptions {
return self.init(path: path)
}
static func evaluate(_ mode: CommandMode) -> Result<GenerateDocsOptions, CommandantError<CommandantError<()>>> {
return create
<*> mode <| Option(key: "path", defaultValue: "rule_docs",
usage: "the directory where the documentation should be saved. defaults to `rule_docs`.")
}
}

View File

@ -0,0 +1,62 @@
import ArgumentParser
extension SwiftLint {
struct Lint: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Print lint warnings and errors")
@OptionGroup
var common: LintOrAnalyzeArguments
@Option(help: pathOptionDescription(for: .lint))
var path: String?
@Flag(help: "Lint standard input.")
var useSTDIN = false
@Flag(help: quietOptionDescription(for: .lint))
var quiet = false
@Option(help: "The directory of the cache used when linting.")
var cachePath: String?
@Flag(help: "Ignore cache when linting.")
var noCache = false
@Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.")
var enableAllRules = false
@Argument(help: pathsArgumentDescription(for: .lint))
var paths = [String]()
mutating func run() throws {
let allPaths: [String]
if let path = path {
allPaths = [path]
} else if !paths.isEmpty {
allPaths = paths
} else {
allPaths = [""] // Lint files in current working directory if no paths were specified.
}
let options = LintOrAnalyzeOptions(
mode: .lint,
paths: allPaths,
useSTDIN: useSTDIN,
configurationFiles: common.config,
strict: common.leniency == .strict,
lenient: common.leniency == .lenient,
forceExclude: common.forceExclude,
useExcludingByPrefix: common.useAlternativeExcluding,
useScriptInputFiles: common.useScriptInputFiles,
benchmark: common.benchmark,
reporter: common.reporter,
quiet: quiet,
cachePath: cachePath,
ignoreCache: noCache,
enableAllRules: enableAllRules,
autocorrect: common.fix,
compilerLogPath: nil,
compileCommands: nil
)
let result = LintOrAnalyzeCommand.run(options)
switch result {
case .success:
return
case .failure(let error):
throw error
}
}
}
}

View File

@ -1,72 +0,0 @@
import Commandant
struct LintCommand: CommandProtocol {
let verb = "lint"
let function = "Print lint warnings and errors (default command)"
func run(_ options: LintOptions) -> Result<(), CommandantError<()>> {
return LintOrAnalyzeCommand.run(LintOrAnalyzeOptions(options))
}
}
struct LintOptions: OptionsProtocol {
let paths: [String]
let useSTDIN: Bool
let configurationFiles: [String]
let strict: Bool
let lenient: Bool
let forceExclude: Bool
let excludeByPrefix: Bool
let useScriptInputFiles: Bool
let benchmark: Bool
let reporter: String
let quiet: Bool
let cachePath: String
let ignoreCache: Bool
let enableAllRules: Bool
// swiftlint:disable line_length
static func create(_ path: String) -> (_ useSTDIN: Bool) -> (_ configurationFiles: [String]) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ excludeByPrefix: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ enableAllRules: Bool) -> (_ paths: [String]) -> LintOptions {
return { useSTDIN in { configurationFiles in { strict in { lenient in { forceExclude in { excludeByPrefix in { useScriptInputFiles in { benchmark in { reporter in { quiet in { cachePath in { ignoreCache in { enableAllRules in { paths in
let allPaths: [String]
if !path.isEmpty {
allPaths = [path]
} else {
allPaths = paths
}
return self.init(paths: allPaths, useSTDIN: useSTDIN, configurationFiles: configurationFiles, strict: strict, lenient: lenient, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, cachePath: cachePath, ignoreCache: ignoreCache, enableAllRules: enableAllRules)
// swiftlint:enable line_length
}}}}}}}}}}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<LintOptions, CommandantError<CommandantError<()>>> {
return create
<*> mode <| pathOption(action: "lint")
<*> mode <| Option(key: "use-stdin", defaultValue: false,
usage: "lint standard input")
<*> mode <| configOption
<*> mode <| Option(key: "strict", defaultValue: false,
usage: "upgrades warnings to serious violations (errors)")
<*> mode <| Option(key: "lenient", defaultValue: false,
usage: "downgrades serious violations to warnings, warning threshold is disabled")
<*> mode <| Option(key: "force-exclude", defaultValue: false,
usage: "exclude files in config `excluded` even if their paths are explicitly specified")
<*> mode <| useAlternativeExcludingOption
<*> mode <| useScriptInputFilesOption
<*> mode <| Option(key: "benchmark", defaultValue: false,
usage: "save benchmarks to benchmark_files.txt " +
"and benchmark_rules.txt")
<*> mode <| Option(key: "reporter", defaultValue: "",
usage: "the reporter used to log errors and warnings")
<*> mode <| quietOption(action: "linting")
<*> mode <| Option(key: "cache-path", defaultValue: "",
usage: "the directory of the cache used when linting")
<*> mode <| Option(key: "no-cache", defaultValue: false,
usage: "ignore cache when linting")
<*> mode <| Option(key: "enable-all-rules", defaultValue: false,
usage: "run all rules, even opt-in and disabled ones, ignoring `only_rules`")
// This should go last to avoid eating other args
<*> mode <| pathsArgument(action: "lint")
}
}

View File

@ -0,0 +1,164 @@
import ArgumentParser
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("Unsupported platform")
#endif
import Foundation
import SwiftLintFramework
import SwiftyTextTable
enum RuleEnablementOptions: String, EnumerableFlag {
case enabled, disabled
static func name(for value: RuleEnablementOptions) -> NameSpecification {
return .shortAndLong
}
static func help(for value: RuleEnablementOptions) -> ArgumentHelp? {
return "Only show \(value.rawValue) rules"
}
}
extension SwiftLint {
struct Rules: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Display the list of rules and their identifiers")
@Option(help: "The path to a SwiftLint configuration file")
var config: String?
@Flag(exclusivity: .exclusive)
var ruleEnablement: RuleEnablementOptions?
@Flag(name: .shortAndLong, help: "Only display correctable rules")
var correctable = false
@Flag(name: .shortAndLong, help: "Display full configuration details")
var verbose = false
@Argument(help: "The rule identifier to display description for")
var ruleID: String?
mutating func run() throws {
if let ruleID = ruleID {
guard let rule = primaryRuleList.list[ruleID] else {
throw SwiftLintError.usageError(description: "No rule with identifier: \(ruleID)")
}
rule.description.printDescription()
return
}
let configuration = Configuration(configurationFiles: [config].compactMap({ $0 }))
let rules = ruleList(configuration: configuration)
let table = TextTable(ruleList: rules, configuration: configuration, verbose: verbose)
print(table.render())
}
private func ruleList(configuration: Configuration) -> RuleList {
guard ruleEnablement != nil || correctable else {
return primaryRuleList
}
let filtered: [Rule.Type] = primaryRuleList.list.compactMap { ruleID, ruleType in
let configuredRule = configuration.rules.first { rule in
return type(of: rule).description.identifier == ruleID
}
if ruleEnablement == .enabled && configuredRule == nil {
return nil
} else if ruleEnablement == .disabled && configuredRule != nil {
return nil
} else if correctable && !(configuredRule is CorrectableRule) {
return nil
}
return ruleType
}
return RuleList(rules: filtered)
}
}
}
private extension RuleDescription {
func printDescription() {
print("\(consoleDescription)")
guard !triggeringExamples.isEmpty else { return }
func indent(_ string: String) -> String {
return string.components(separatedBy: "\n")
.map { " \($0)" }
.joined(separator: "\n")
}
print("\nTriggering Examples (violation is marked with '↓'):")
for (index, example) in triggeringExamples.enumerated() {
print("\nExample #\(index + 1)\n\n\(indent(example.code))")
}
}
}
// MARK: - SwiftyTextTable
private extension TextTable {
init(ruleList: RuleList, configuration: Configuration, verbose: Bool) {
let columns = [
TextTableColumn(header: "identifier"),
TextTableColumn(header: "opt-in"),
TextTableColumn(header: "correctable"),
TextTableColumn(header: "enabled in your config"),
TextTableColumn(header: "kind"),
TextTableColumn(header: "analyzer"),
TextTableColumn(header: "configuration")
]
self.init(columns: columns)
let sortedRules = ruleList.list.sorted { $0.0 < $1.0 }
func truncate(_ string: String) -> String {
let stringWithNoNewlines = string.replacingOccurrences(of: "\n", with: "\\n")
let minWidth = "configuration".count - "...".count
let configurationStartColumn = 124
let maxWidth = verbose ? Int.max : Terminal.currentWidth()
let truncatedEndIndex = stringWithNoNewlines.index(
stringWithNoNewlines.startIndex,
offsetBy: max(minWidth, maxWidth - configurationStartColumn),
limitedBy: stringWithNoNewlines.endIndex
)
if let truncatedEndIndex = truncatedEndIndex {
return stringWithNoNewlines[..<truncatedEndIndex] + "..."
}
return stringWithNoNewlines
}
for (ruleID, ruleType) in sortedRules {
let rule = ruleType.init()
let configuredRule = configuration.rules.first { rule in
guard type(of: rule).description.identifier == ruleID else {
return false
}
guard let customRules = rule as? CustomRules else {
return true
}
return !customRules.configuration.customRuleConfigurations.isEmpty
}
addRow(values: [
ruleID,
(rule is OptInRule) ? "yes" : "no",
(rule is CorrectableRule) ? "yes" : "no",
configuredRule != nil ? "yes" : "no",
ruleType.description.kind.rawValue,
(rule is AnalyzerRule) ? "yes" : "no",
truncate((configuredRule ?? rule).configurationDescription)
])
}
}
}
private struct Terminal {
static func currentWidth() -> Int {
var size = winsize()
#if os(Linux)
_ = ioctl(CInt(STDOUT_FILENO), UInt(TIOCGWINSZ), &size)
#else
_ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)
#endif
return Int(size.ws_col)
}
}

View File

@ -1,182 +0,0 @@
import Commandant
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("Unsupported platform")
#endif
import SwiftLintFramework
import SwiftyTextTable
private func print(ruleDescription desc: RuleDescription) {
print("\(desc.consoleDescription)")
if !desc.triggeringExamples.isEmpty {
func indent(_ string: String) -> String {
return string.components(separatedBy: "\n")
.map { " \($0)" }
.joined(separator: "\n")
}
print("\nTriggering Examples (violation is marked with '↓'):")
for (index, example) in desc.triggeringExamples.enumerated() {
print("\nExample #\(index + 1)\n\n\(indent(example.code))")
}
}
}
struct RulesCommand: CommandProtocol {
let verb = "rules"
let function = "Display the list of rules and their identifiers"
func run(_ options: RulesOptions) -> Result<(), CommandantError<()>> {
if let ruleID = options.ruleID {
guard let rule = primaryRuleList.list[ruleID] else {
return .failure(.usageError(description: "No rule with identifier: \(ruleID)"))
}
print(ruleDescription: rule.description)
return .success(())
}
if options.onlyDisabledRules && options.onlyEnabledRules {
return .failure(.usageError(description: "You can't use --disabled and --enabled at the same time."))
}
let configuration = Configuration(options: options)
let rules = ruleList(for: options, configuration: configuration)
print(TextTable(ruleList: rules, configuration: configuration, verbose: options.verbose).render())
return .success(())
}
private func ruleList(for options: RulesOptions, configuration: Configuration) -> RuleList {
guard options.onlyEnabledRules || options.onlyDisabledRules || options.onlyCorrectableRules else {
return primaryRuleList
}
let filtered: [Rule.Type] = primaryRuleList.list.compactMap { ruleID, ruleType in
let configuredRule = configuration.rules.first { rule in
return type(of: rule).description.identifier == ruleID
}
if options.onlyEnabledRules && configuredRule == nil {
return nil
} else if options.onlyDisabledRules && configuredRule != nil {
return nil
} else if options.onlyCorrectableRules && !(configuredRule is CorrectableRule) {
return nil
}
return ruleType
}
return RuleList(rules: filtered)
}
}
struct RulesOptions: OptionsProtocol {
fileprivate let ruleID: String?
let configurationFiles: [String]
fileprivate let onlyEnabledRules: Bool
fileprivate let onlyDisabledRules: Bool
fileprivate let onlyCorrectableRules: Bool
fileprivate let verbose: Bool
// swiftlint:disable line_length
static func create(_ configurationFiles: [String]) -> (_ ruleID: String) -> (_ onlyEnabledRules: Bool) -> (_ onlyDisabledRules: Bool) -> (_ onlyCorrectableRules: Bool) -> (_ verbose: Bool) -> RulesOptions {
return { ruleID in { onlyEnabledRules in { onlyDisabledRules in { onlyCorrectableRules in { verbose in
self.init(ruleID: (ruleID.isEmpty ? nil : ruleID),
configurationFiles: configurationFiles,
onlyEnabledRules: onlyEnabledRules,
onlyDisabledRules: onlyDisabledRules,
onlyCorrectableRules: onlyCorrectableRules,
verbose: verbose)
}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<RulesOptions, CommandantError<CommandantError<()>>> {
return create
<*> mode <| configOption
<*> mode <| Argument(defaultValue: "",
usage: "the rule identifier to display description for")
<*> mode <| Switch(flag: "e",
key: "enabled",
usage: "only display enabled rules")
<*> mode <| Switch(flag: "d",
key: "disabled",
usage: "only display disabled rules")
<*> mode <| Switch(flag: "c",
key: "correctable",
usage: "only display correctable rules")
<*> mode <| Switch(flag: "v",
key: "verbose",
usage: "display full configuration detail")
}
}
// MARK: - SwiftyTextTable
extension TextTable {
init(ruleList: RuleList, configuration: Configuration, verbose: Bool) {
let columns = [
TextTableColumn(header: "identifier"),
TextTableColumn(header: "opt-in"),
TextTableColumn(header: "correctable"),
TextTableColumn(header: "enabled in your config"),
TextTableColumn(header: "kind"),
TextTableColumn(header: "analyzer"),
TextTableColumn(header: "configuration")
]
self.init(columns: columns)
let sortedRules = ruleList.list.sorted { $0.0 < $1.0 }
func truncate(_ string: String) -> String {
let stringWithNoNewlines = string.replacingOccurrences(of: "\n", with: "\\n")
let minWidth = "configuration".count - "...".count
let configurationStartColumn = 124
let maxWidth = verbose ? Int.max : Terminal.currentWidth()
let truncatedEndIndex = stringWithNoNewlines.index(
stringWithNoNewlines.startIndex,
offsetBy: max(minWidth, maxWidth - configurationStartColumn),
limitedBy: stringWithNoNewlines.endIndex
)
if let truncatedEndIndex = truncatedEndIndex {
return stringWithNoNewlines[..<truncatedEndIndex] + "..."
}
return stringWithNoNewlines
}
for (ruleID, ruleType) in sortedRules {
let rule = ruleType.init()
let configuredRule = configuration.rules.first { rule in
guard type(of: rule).description.identifier == ruleID else {
return false
}
guard let customRules = rule as? CustomRules else {
return true
}
return !customRules.configuration.customRuleConfigurations.isEmpty
}
addRow(values: [
ruleID,
(rule is OptInRule) ? "yes" : "no",
(rule is CorrectableRule) ? "yes" : "no",
configuredRule != nil ? "yes" : "no",
ruleType.description.kind.rawValue,
(rule is AnalyzerRule) ? "yes" : "no",
truncate((configuredRule ?? rule).configurationDescription)
])
}
}
}
struct Terminal {
static func currentWidth() -> Int {
var size = winsize()
#if os(Linux)
_ = ioctl(CInt(STDOUT_FILENO), UInt(TIOCGWINSZ), &size)
#else
_ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)
#endif
return Int(size.ws_col)
}
}

View File

@ -1,30 +0,0 @@
import Commandant
import Foundation
struct ShowDocsCommand: CommandProtocol {
let verb = "docs"
let function = "Open SwiftLint Docs on web browser"
func run(_ options: NoOptions<CommandantError<()>>) -> Result<(), CommandantError<()>> {
let url = URL(string: "https://realm.github.io/SwiftLint")!
open(url)
return .success(())
}
}
private extension ShowDocsCommand {
func open(_ url: URL) {
let process = Process()
#if os(Linux)
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
let command = "xdg-open"
process.arguments = [command, url.absoluteString]
try? process.run()
#else
process.launchPath = "/usr/bin/env"
let command = "open"
process.arguments = [command, url.absoluteString]
process.launch()
#endif
}
}

View File

@ -0,0 +1,38 @@
import ArgumentParser
import SwiftLintFramework
struct SwiftLint: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "swiftlint",
abstract: "A tool to enforce Swift style and conventions.",
version: Version.value,
subcommands: [
Analyze.self,
Docs.self,
GenerateDocs.self,
Lint.self,
Rules.self,
Version.self
],
defaultSubcommand: Lint.self
)
// Temporary convenience to help migrate users to the new command.
static func mainHandlingDeprecatedCommands(_ arguments: [String]? = nil) {
let argumentsToCheck = arguments ?? Array(CommandLine.arguments.dropFirst())
guard argumentsToCheck.first == "autocorrect" else {
main(arguments)
return
}
queuedPrintError(
"""
The `swiftlint autocorrect` command is no longer available.
Please use `swiftlint --fix` instead.
"""
)
var newArguments = argumentsToCheck
newArguments[0] = "--fix"
main(newArguments)
}
}

View File

@ -0,0 +1,14 @@
import ArgumentParser
import SwiftLintFramework
extension SwiftLint {
struct Version: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Display the current version of SwiftLint")
static var value: String { SwiftLintFramework.Version.current.value }
mutating func run() throws {
print(Self.value)
}
}
}

View File

@ -1,12 +0,0 @@
import Commandant
import SwiftLintFramework
struct VersionCommand: CommandProtocol {
let verb = "version"
let function = "Display the current version of SwiftLint"
func run(_ options: NoOptions<CommandantError<()>>) -> Result<(), CommandantError<()>> {
print(Version.current.value)
return .success(())
}
}

View File

@ -1,4 +1,3 @@
import Commandant
import Dispatch
import Foundation
import SourceKittenFramework
@ -6,8 +5,8 @@ import SwiftLintFramework
private let indexIncrementerQueue = DispatchQueue(label: "io.realm.swiftlint.indexIncrementer")
private func scriptInputFiles() -> Result<[SwiftLintFile], CommandantError<()>> {
func getEnvironmentVariable(_ variable: String) -> Result<String, CommandantError<()>> {
private func scriptInputFiles() -> Result<[SwiftLintFile], SwiftLintError> {
func getEnvironmentVariable(_ variable: String) -> Result<String, SwiftLintError> {
let environment = ProcessInfo.processInfo.environment
if let value = environment[variable] {
return .success(value)
@ -15,7 +14,7 @@ private func scriptInputFiles() -> Result<[SwiftLintFile], CommandantError<()>>
return .failure(.usageError(description: "Environment variable not set: \(variable)"))
}
let count: Result<Int, CommandantError<()>> = {
let count: Result<Int, SwiftLintError> = {
let inputFileKey = "SCRIPT_INPUT_FILE_COUNT"
guard let countString = ProcessInfo.processInfo.environment[inputFileKey] else {
return .failure(.usageError(description: "\(inputFileKey) variable not set"))
@ -48,7 +47,7 @@ private func autoreleasepool<T>(block: () -> T) -> T { return block() }
extension Configuration {
func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage)
-> Result<[SwiftLintFile], CommandantError<()>> {
-> Result<[SwiftLintFile], SwiftLintError> {
return getFiles(with: visitor)
.flatMap { groupFiles($0, visitor: visitor) }
.map { linters(for: $0, visitor: visitor) }
@ -59,7 +58,7 @@ extension Configuration {
private func groupFiles(_ files: [SwiftLintFile],
visitor: LintableFilesVisitor)
-> Result<[Configuration: [SwiftLintFile]], CommandantError<()>> {
-> Result<[Configuration: [SwiftLintFile]], SwiftLintError> {
if files.isEmpty && !visitor.allowZeroLintableFiles {
let errorMessage = "No lintable files found at paths: '\(visitor.paths.joined(separator: ", "))'"
return .failure(.usageError(description: errorMessage))
@ -188,7 +187,7 @@ extension Configuration {
return visitor.parallel ? linters.parallelMap(transform: visit) : linters.map(visit)
}
fileprivate func getFiles(with visitor: LintableFilesVisitor) -> Result<[SwiftLintFile], CommandantError<()>> {
fileprivate func getFiles(with visitor: LintableFilesVisitor) -> Result<[SwiftLintFile], SwiftLintError> {
if visitor.useSTDIN {
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
if let stdinString = String(data: stdinData, encoding: .utf8) {
@ -225,20 +224,9 @@ extension Configuration {
})
}
// MARK: LintOrAnalyze Command
init(options: LintOrAnalyzeOptions) {
let cachePath = options.cachePath.isEmpty ? nil : options.cachePath
self.init(
configurationFiles: options.configurationFiles,
enableAllRules: options.enableAllRules,
cachePath: cachePath
)
}
func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil, storage: RuleStorage,
visitorBlock: @escaping (CollectedLinter) -> Void)
-> Result<[SwiftLintFile], CommandantError<()>> {
-> Result<[SwiftLintFile], SwiftLintError> {
return LintableFilesVisitor.create(options,
cache: cache,
allowZeroLintableFiles: allowZeroLintableFiles,
@ -247,20 +235,13 @@ extension Configuration {
})
}
// MARK: AutoCorrect Command
// MARK: LintOrAnalyze Command
init(options: AutoCorrectOptions) {
let cachePath = options.cachePath.isEmpty ? nil : options.cachePath
init(options: LintOrAnalyzeOptions) {
self.init(
configurationFiles: options.configurationFiles,
cachePath: cachePath
)
}
// MARK: Rules command
init(options: RulesOptions) {
self.init(
configurationFiles: options.configurationFiles
enableAllRules: options.enableAllRules,
cachePath: options.cachePath
)
}
}

View File

@ -11,7 +11,6 @@ extension Reporter {
}
}
func reporterFrom(optionsReporter: String, configuration: Configuration) -> Reporter.Type {
let string = optionsReporter.isEmpty ? configuration.reporter : optionsReporter
return reporterFrom(identifier: string)
func reporterFrom(optionsReporter: String?, configuration: Configuration) -> Reporter.Type {
return reporterFrom(identifier: optionsReporter ?? configuration.reporter)
}

View File

@ -1,34 +0,0 @@
import Commandant
func pathOption(action: String) -> Option<String> {
return Option(key: "path",
defaultValue: "",
usage: "the path to the file or directory to \(action)")
}
func pathsArgument(action: String) -> Argument<[String]> {
return Argument(defaultValue: [""],
usage: "list of paths to the files or directories to \(action)")
}
let configOption = Option(key: "config",
defaultValue: [String](),
usage: "the path to one or more SwiftLint configuration files, "
+ "evaluated as a parent-child hierarchy")
let useScriptInputFilesOption = Option(key: "use-script-input-files",
defaultValue: false,
usage: "read SCRIPT_INPUT_FILE* environment variables " +
"as files")
let useAlternativeExcludingOption = Option(key: "use-alternative-excluding",
defaultValue: false,
usage: "alternative exclude paths algorithm for `excluded`." +
"may speed up excluding for some cases")
func quietOption(action: String) -> Option<Bool> {
return Option(key: "quiet",
defaultValue: false,
usage: "don't print status logs like '\(action.capitalized) <file>' & " +
"'Done \(action)'")
}

View File

@ -0,0 +1,52 @@
import ArgumentParser
enum LeniencyOptions: String, EnumerableFlag {
case strict, lenient
static func help(for value: LeniencyOptions) -> ArgumentHelp? {
switch value {
case .strict:
return "Upgrades warnings to serious violations (errors)."
case .lenient:
return "Downgrades serious violations to warnings, warning threshold is disabled."
}
}
}
// MARK: - Common Arguments
struct LintOrAnalyzeArguments: ParsableArguments {
@Option(help: "The path to one or more SwiftLint configuration files, evaluated as a parent-child hierarchy.")
var config = [String]()
@Flag(name: [.long, .customLong("autocorrect")], help: "Correct violations whenever possible.")
var fix = false
@Flag(help: "Use an alternative algorithm to exclude paths for `excluded`, which may be faster in some cases.")
var useAlternativeExcluding = false
@Flag(help: "Read SCRIPT_INPUT_FILE* environment variables as files.")
var useScriptInputFiles = false
@Flag(exclusivity: .exclusive)
var leniency: LeniencyOptions?
@Flag(help: "Exclude files in config `excluded` even if their paths are explicitly specified.")
var forceExclude = false
@Flag(help: "Save benchmarks to `benchmark_files.txt` and `benchmark_rules.txt`.")
var benchmark = false
@Option(help: "The reporter used to log errors and warnings.")
var reporter: String?
}
// MARK: - Common Argument Help
// It'd be great to be able to parameterize an `@OptionGroup` so we could move these options into
// `LintOrAnalyzeArguments`.
func pathOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
"The path to the file or directory to \(mode.imperative)."
}
func pathsArgumentDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
"List of paths to the files or directories to \(mode.imperative)."
}
func quietOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp {
"Don't print status logs like '\(mode.verb.capitalized) <file>' & 'Done \(mode.verb)'."
}

View File

@ -1,4 +1,3 @@
import Commandant
import Dispatch
import Foundation
import SwiftLintFramework
@ -6,6 +5,15 @@ import SwiftLintFramework
enum LintOrAnalyzeMode {
case lint, analyze
var imperative: String {
switch self {
case .lint:
return "lint"
case .analyze:
return "analyze"
}
}
var verb: String {
switch self {
case .lint:
@ -17,7 +25,11 @@ enum LintOrAnalyzeMode {
}
struct LintOrAnalyzeCommand {
static func run(_ options: LintOrAnalyzeOptions) -> Result<(), CommandantError<()>> {
static func run(_ options: LintOrAnalyzeOptions) -> Result<(), SwiftLintError> {
return options.autocorrect ? autocorrect(options) : lintOrAnalyze(options)
}
private static func lintOrAnalyze(_ options: LintOrAnalyzeOptions) -> Result<(), SwiftLintError> {
var fileBenchmark = Benchmark(name: "files")
var ruleBenchmark = Benchmark(name: "rules")
var violations = [StyleViolation]()
@ -125,6 +137,26 @@ struct LintOrAnalyzeCommand {
queuedFatalError("Invalid command line options: 'lenient' and 'strict' are mutually exclusive.")
}
}
private static func autocorrect(_ options: LintOrAnalyzeOptions) -> Result<(), SwiftLintError> {
let storage = RuleStorage()
let configuration = Configuration(options: options)
return configuration.visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
let corrections = linter.correct(using: storage)
if !corrections.isEmpty && !options.quiet {
let correctionLogs = corrections.map({ $0.consoleDescription })
queuedPrint(correctionLogs.joined(separator: "\n"))
}
}.flatMap { files in
if !options.quiet {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
}
return .success(())
}
}
}
struct LintOrAnalyzeOptions {
@ -138,56 +170,14 @@ struct LintOrAnalyzeOptions {
let useExcludingByPrefix: Bool
let useScriptInputFiles: Bool
let benchmark: Bool
let reporter: String
let reporter: String?
let quiet: Bool
let cachePath: String
let cachePath: String?
let ignoreCache: Bool
let enableAllRules: Bool
let autocorrect: Bool
let compilerLogPath: String
let compileCommands: String
init(_ options: LintOptions) {
mode = .lint
paths = options.paths
useSTDIN = options.useSTDIN
configurationFiles = options.configurationFiles
strict = options.strict
lenient = options.lenient
forceExclude = options.forceExclude
useExcludingByPrefix = options.excludeByPrefix
useScriptInputFiles = options.useScriptInputFiles
benchmark = options.benchmark
reporter = options.reporter
quiet = options.quiet
cachePath = options.cachePath
ignoreCache = options.ignoreCache
enableAllRules = options.enableAllRules
autocorrect = false
compilerLogPath = ""
compileCommands = ""
}
init(_ options: AnalyzeOptions) {
mode = .analyze
paths = options.paths
useSTDIN = false
configurationFiles = options.configurationFiles
strict = options.strict
lenient = options.lenient
forceExclude = options.forceExclude
useExcludingByPrefix = options.excludeByPrefix
useScriptInputFiles = options.useScriptInputFiles
benchmark = options.benchmark
reporter = options.reporter
quiet = options.quiet
cachePath = ""
ignoreCache = true
enableAllRules = options.enableAllRules
autocorrect = options.autocorrect
compilerLogPath = options.compilerLogPath
compileCommands = options.compileCommands
}
let compilerLogPath: String?
let compileCommands: String?
var verb: String {
if autocorrect {

View File

@ -1,4 +1,3 @@
import Commandant
import Foundation
import SourceKittenFramework
import SwiftLintFramework
@ -96,7 +95,7 @@ struct LintableFilesVisitor {
cache: LinterCache?,
allowZeroLintableFiles: Bool,
block: @escaping (CollectedLinter) -> Void)
-> Result<LintableFilesVisitor, CommandantError<()>> {
-> Result<LintableFilesVisitor, SwiftLintError> {
let compilerInvocations: CompilerInvocations?
if options.mode == .lint {
compilerInvocations = nil
@ -141,9 +140,8 @@ struct LintableFilesVisitor {
}
private static func loadCompilerInvocations(_ options: LintOrAnalyzeOptions)
-> Result<CompilerInvocations, CommandantError<()>> {
if !options.compilerLogPath.isEmpty {
let path = options.compilerLogPath
-> Result<CompilerInvocations, SwiftLintError> {
if let path = options.compilerLogPath {
guard let compilerInvocations = self.loadLogCompilerInvocations(path) else {
return .failure(
.usageError(description: "Could not read compiler log at path: '\(path)'")
@ -151,17 +149,14 @@ struct LintableFilesVisitor {
}
return .success(.buildLog(compilerInvocations: compilerInvocations))
} else if !options.compileCommands.isEmpty {
let path = options.compileCommands
} else if let path = options.compileCommands {
switch self.loadCompileCommands(path) {
case .success(let compileCommands):
return .success(.compilationDatabase(compileCommands: compileCommands))
case .failure(let error):
return .failure(
.usageError(
description: "Could not read compilation database at path: '\(path)' \(error.description)"
)
)
return .failure(.usageError(
description: "Could not read compilation database at path: '\(path)' \(error.localizedDescription)"
))
}
}
@ -220,14 +215,14 @@ struct LintableFilesVisitor {
}
}
private enum CompileCommandsLoadError: Error {
private enum CompileCommandsLoadError: LocalizedError {
case nonExistentFile(String)
case malformedCommands(String)
case malformedFile(String, Int)
case malformedArguments(String, Int)
case missingFileInArguments(String, Int, [String])
var description: String {
var errorDescription: String? {
switch self {
case let .nonExistentFile(path):
return "Could not read compile commands file at '\(path)'"

View File

@ -0,0 +1,12 @@
import Foundation
enum SwiftLintError: LocalizedError {
case usageError(description: String)
var errorDescription: String? {
switch self {
case .usageError(let description):
return description
}
}
}

View File

@ -1,21 +1,15 @@
import Commandant
import Dispatch
import SwiftLintFramework
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#else
#error("Unsupported platform")
#endif
DispatchQueue.global().async {
let registry = CommandRegistry<CommandantError<()>>()
registry.register(LintCommand())
registry.register(AutoCorrectCommand())
registry.register(AnalyzeCommand())
registry.register(VersionCommand())
registry.register(RulesCommand())
registry.register(GenerateDocsCommand())
registry.register(ShowDocsCommand())
registry.register(HelpCommand(registry: registry))
registry.main(defaultVerb: LintCommand().verb) { error in
queuedPrintError(String(describing: error))
}
SwiftLint.mainHandlingDeprecatedCommands()
exit(EXIT_SUCCESS)
}
dispatchMain()

View File

@ -10,6 +10,6 @@ Pod::Spec.new do |s|
s.source_files = 'Source/SwiftLintFramework/**/*.swift'
s.swift_versions = ['5.2', '5.3']
s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
s.dependency 'SourceKittenFramework', '~> 0.30.1'
s.dependency 'SourceKittenFramework', '~> 0.31.0'
s.dependency 'Yams', '~> 4.0.2'
end