Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add support for compilation databases #2962

Merged
merged 14 commits into from
Dec 3, 2019
10 changes: 10 additions & 0 deletions Source/SwiftLintFramework/Models/YamlParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public struct YamlParser {
throw YamlParserError.yamlParsing("\(error)")
}
}

public static func parseArray(_ yaml: String,
env: [String: String] = ProcessInfo.processInfo.environment) throws -> [[String: Any]] {
do {
return try Yams.load(yaml: yaml, .default,
.swiftlintContructor(env: env)) as? [[String: Any]] ?? []
} catch {
throw YamlParserError.yamlParsing("\(error)")
}
}
}

private extension Constructor {
Expand Down
11 changes: 7 additions & 4 deletions Source/swiftlint/Commands/AnalyzeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,20 @@ struct AnalyzeOptions: OptionsProtocol {
let enableAllRules: Bool
let autocorrect: Bool
let compilerLogPath: String
let compileCommands: String

// swiftlint:disable line_length
static func create(_ path: String) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ enableAllRules: Bool) -> (_ autocorrect: Bool) -> (_ compilerLogPath: String) -> (_ paths: [String]) -> AnalyzeOptions {
return { configurationFile in { strict in { lenient in { forceExclude in { useScriptInputFiles in { benchmark in { reporter in { quiet in { enableAllRules in { autocorrect in { compilerLogPath in { paths in
static func create(_ path: String) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ enableAllRules: Bool) -> (_ autocorrect: Bool) -> (_ compilerLogPath: String) -> (_ compileCommands: String) -> (_ paths: [String]) -> AnalyzeOptions {
return { configurationFile in { strict in { lenient in { forceExclude 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, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, enableAllRules: enableAllRules, autocorrect: autocorrect, compilerLogPath: compilerLogPath)
return self.init(paths: allPaths, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, 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<()>>> {
Expand All @@ -86,6 +87,8 @@ struct AnalyzeOptions: OptionsProtocol {
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")
}
Expand Down
3 changes: 3 additions & 0 deletions Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ struct LintOrAnalyzeOptions {
let enableAllRules: Bool
let autocorrect: Bool
let compilerLogPath: String
let compileCommands: String

init(_ options: LintOptions) {
mode = .lint
Expand All @@ -160,6 +161,7 @@ struct LintOrAnalyzeOptions {
enableAllRules = options.enableAllRules
autocorrect = false
compilerLogPath = ""
compileCommands = ""
}

init(_ options: AnalyzeOptions) {
Expand All @@ -179,6 +181,7 @@ struct LintOrAnalyzeOptions {
enableAllRules = options.enableAllRules
autocorrect = options.autocorrect
compilerLogPath = options.compilerLogPath
compileCommands = options.compileCommands
}

var verb: String {
Expand Down
74 changes: 49 additions & 25 deletions Source/swiftlint/Helpers/LintableFilesVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,27 @@ import Foundation
import SourceKittenFramework
import SwiftLintFramework

enum CompilerInvocations {
case buildLog(compilerInvocations: [String])
case compilationDatabase(compileCommands: [[String: Any]])

func arguments(forFile path: String?) -> [String] {
return path.flatMap { path in
switch self {
case let .buildLog(compilerInvocations):
return CompilerArgumentsExtractor.compilerArgumentsForFile(path, compilerInvocations: compilerInvocations)
case let .compilationDatabase(compileCommands):
return compileCommands
.first { $0["file"] as? String == path }
.flatMap { $0["arguments"] as? [String] }
}
} ?? []
}
}

enum LintOrAnalyzeModeWithCompilerArguments {
case lint
case analyze(allCompilerInvocations: [String])
case analyze(allCompilerInvocations: CompilerInvocations)
}

struct LintableFilesVisitor {
Expand Down Expand Up @@ -35,7 +53,7 @@ struct LintableFilesVisitor {
}

private init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, useScriptInputFiles: Bool,
forceExclude: Bool, cache: LinterCache?, compilerLogContents: String,
forceExclude: Bool, cache: LinterCache?, compilerInvocations: CompilerInvocations?,
block: @escaping (CollectedLinter) -> Void) {
self.paths = paths
self.action = action
Expand All @@ -45,46 +63,54 @@ struct LintableFilesVisitor {
self.forceExclude = forceExclude
self.cache = cache
self.parallel = true
if compilerLogContents.isEmpty {
self.mode = .lint
if let compilerInvocations = compilerInvocations {
self.mode = .analyze(allCompilerInvocations: compilerInvocations)
} else {
let allCompilerInvocations = CompilerArgumentsExtractor
.allCompilerInvocations(compilerLogs: compilerLogContents)
self.mode = .analyze(allCompilerInvocations: allCompilerInvocations)
self.mode = .lint
}
self.block = block
}

static func create(_ options: LintOrAnalyzeOptions, cache: LinterCache?, block: @escaping (CollectedLinter) -> Void)
-> Result<LintableFilesVisitor, CommandantError<()>> {
let compilerLogContents: String
let compilerInvocations: CompilerInvocations?
if options.mode == .lint {
compilerLogContents = ""
} else if let logContents = LintableFilesVisitor.compilerLogContents(logPath: options.compilerLogPath),
!logContents.isEmpty {
compilerLogContents = logContents
compilerInvocations = nil
} else {
return .failure(
.usageError(description: "Could not read compiler log at path: '\(options.compilerLogPath)'")
)
if let logContents = LintableFilesVisitor.compilerLogContents(logPath: options.compilerLogPath) {
let allCompilerInvocations = CompilerArgumentsExtractor.allCompilerInvocations(compilerLogs: logContents)
compilerInvocations = .buildLog(compilerInvocations: allCompilerInvocations)
} else if !options.compileCommands.isEmpty {
do {
let yamlContents = try String(contentsOfFile: options.compileCommands, encoding: .utf8)
let compileCommands = try YamlParser.parseArray(yamlContents)
compilerInvocations = .compilationDatabase(compileCommands: compileCommands)
} catch {
return .failure(
.usageError(description: "Could not compilation database at path: '\(options.compileCommands)'")
)
}
} else {
return .failure(
.usageError(description: "Could not read compiler log at path: '\(options.compilerLogPath)'")
)
}
}

let visitor = LintableFilesVisitor(paths: options.paths, action: options.verb.bridge().capitalized,
useSTDIN: options.useSTDIN, quiet: options.quiet,
useScriptInputFiles: options.useScriptInputFiles,
forceExclude: options.forceExclude, cache: cache,
compilerLogContents: compilerLogContents, block: block)
compilerInvocations: compilerInvocations, block: block)
return .success(visitor)
}

func shouldSkipFile(atPath path: String?) -> Bool {
switch self.mode {
case .lint:
return false
case let .analyze(allCompilerInvocations):
let compilerArguments = path.flatMap {
CompilerArgumentsExtractor.compilerArgumentsForFile($0, compilerInvocations: allCompilerInvocations)
} ?? []
case let .analyze(compilerInvocations):
let compilerArguments = compilerInvocations.arguments(forFile: path)
return compilerArguments.isEmpty
}
}
Expand All @@ -93,10 +119,8 @@ struct LintableFilesVisitor {
switch self.mode {
case .lint:
return Linter(file: file, configuration: configuration, cache: cache)
case let .analyze(allCompilerInvocations):
let compilerArguments = file.path.flatMap {
CompilerArgumentsExtractor.compilerArgumentsForFile($0, compilerInvocations: allCompilerInvocations)
} ?? []
case let .analyze(compilerInvocations):
let compilerArguments = compilerInvocations.arguments(forFile: file.path)
return Linter(file: file, configuration: configuration, compilerArguments: compilerArguments)
}
}
Expand All @@ -108,7 +132,7 @@ struct LintableFilesVisitor {

if let data = FileManager.default.contents(atPath: logPath),
let logContents = String(data: data, encoding: .utf8) {
return logContents
return logContents.isEmpty ? nil : logContents
}

print("couldn't read log file at path '\(logPath)'")
Expand Down