Compare commits
13 Commits
main
...
use-index-
Author | SHA1 | Date |
---|---|---|
![]() |
31fb736284 | |
![]() |
00f6a76050 | |
![]() |
dcf67cac3c | |
![]() |
e207dcc16f | |
![]() |
0e6dfb9da2 | |
![]() |
0bec408655 | |
![]() |
9adb2deac2 | |
![]() |
1aec09282d | |
![]() |
d781a1261e | |
![]() |
783976aa52 | |
![]() |
bb2af8c5b3 | |
![]() |
3a39d8bb49 | |
![]() |
88283dc547 |
|
@ -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
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"] : [])
|
||||
),
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`.")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)'")
|
||||
}
|
|
@ -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)'."
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)'"
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
|
||||
enum SwiftLintError: LocalizedError {
|
||||
case usageError(description: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .usageError(let description):
|
||||
return description
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue