Files
SwiftLint/Source/swiftlint/Helpers/CompilerArgumentsExtractor.swift
T
JP Simard 2bcea4b04d Add ability for SwiftLint to lint files with full type-checked AST awareness (#2379)
* Add LintableFilesVisitor

* Move LintCommand logic into LintOrAnalyzeCommand

to prepare for the upcoming analyze command

* Add AnalyzeCommand (not fully implemented yet in SwiftLintFramework)

* Add analyzerRules configuration member

* Add AnalyzerRule protocol

* Pass compiler arguments to validate/correct

* Add requiresFileOnDisk member to RuleDescription

This will be used by AnalyzerRules because they need a file on disk
to pass in the compiler arguments to SourceKit.

* Exclusively run AnalyzerRules when the Linter has compiler arguments

* Enable testing AnalyzerRules in TestHelpers

* Add ExplicitSelfRule

* Update documentation

* Fix `analyze --autocorrect`

* Improve performance of CompilerArgumentsExtractor

* Fix lint command actually running analyze

* Move File operations in TestHelpers into a private extension

* Add analyzer column to rules command and markdown documentation

* Use a Set literal

* Make AnalyzerRule inherit from OptInRule

* Mention analyzer_rules in readme

* Mention that analyzer rules are slow
2018-09-02 00:13:27 -07:00

121 lines
3.7 KiB
Swift

import Foundation
import SourceKittenFramework
struct CompilerArgumentsExtractor {
static func allCompilerInvocations(compilerLogs: String) -> [String] {
var compilerInvocations = [String]()
compilerLogs.enumerateLines { line, _ in
if let swiftcIndex = line.range(of: "swiftc ")?.upperBound, line.contains(" -module-name ") {
compilerInvocations.append(String(line[swiftcIndex...]))
}
}
return compilerInvocations
}
static func compilerArgumentsForFile(_ sourceFile: String, compilerInvocations: [String]) -> [String]? {
let escapedSourceFile = sourceFile.replacingOccurrences(of: " ", with: "\\ ")
guard let compilerInvocation = compilerInvocations.first(where: { $0.contains(escapedSourceFile) }) else {
return nil
}
return parseCLIArguments(compilerInvocation)
}
}
// MARK: - Private
#if !os(Linux)
private extension Scanner {
func scanUpToString(_ string: String) -> String? {
var result: NSString?
let success = scanUpTo(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
func scanString(_ string: String) -> String? {
var result: NSString?
let success = scanString(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
}
#endif
private func parseCLIArguments(_ string: String) -> [String] {
let escapedSpacePlaceholder = "\u{0}"
let scanner = Scanner(string: string)
var str = ""
var didStart = false
while let result = scanner.scanUpToString("\"") {
if didStart {
str += result.replacingOccurrences(of: " ", with: escapedSpacePlaceholder)
str += " "
} else {
str += result
}
_ = scanner.scanString("\"")
didStart = !didStart
}
return filter(arguments:
str.trimmingCharacters(in: .whitespaces)
.replacingOccurrences(of: "\\ ", with: escapedSpacePlaceholder)
.components(separatedBy: " ")
.map { $0.replacingOccurrences(of: escapedSpacePlaceholder, with: " ") }
)
}
/**
Partially filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: A tuple of partially filtered compiler arguments in `.0`, and whether or not there are
more flags to remove in `.1`.
*/
private func partiallyFilter(arguments args: [String]) -> ([String], Bool) {
guard let indexOfFlagToRemove = args.index(of: "-output-file-map") else {
return (args, false)
}
var args = args
args.remove(at: args.index(after: indexOfFlagToRemove))
args.remove(at: indexOfFlagToRemove)
return (args, true)
}
/**
Filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: Filtered compiler arguments.
*/
private func filter(arguments args: [String]) -> [String] {
var args = args
args.append(contentsOf: ["-D", "DEBUG"])
var shouldContinueToFilterArguments = true
while shouldContinueToFilterArguments {
(args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args)
}
return args.filter {
![
"-parseable-output",
"-incremental",
"-serialize-diagnostics",
"-emit-dependencies"
].contains($0)
}.map {
if $0 == "-O" {
return "-Onone"
} else if $0 == "-DNDEBUG=1" {
return "-DDEBUG=1"
}
return $0
}
}