Skip to content

Commit

Permalink
Write custom script tests + some refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeehut committed Sep 7, 2021
1 parent 5bd9ac7 commit e7cc711
Show file tree
Hide file tree
Showing 24 changed files with 316 additions and 146 deletions.
26 changes: 16 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PackageDescription

let package = Package(
name: "AnyLint",
platforms: [.macOS(.v10_12)],
platforms: [.macOS(.v10_15)],
products: [
.executable(name: "anylint", targets: ["Commands"]),
],
Expand Down Expand Up @@ -36,7 +36,14 @@ let package = Package(
.product(name: "Rainbow", package: "Rainbow"),
]
),
.target(name: "Checkers", dependencies: ["Core"]),
.target(
name: "Checkers",
dependencies: [
"Core",
"Reporting",
.product(name: "ShellOut", package: "ShellOut"),
]
),
.target(
name: "Configuration",
dependencies: [
Expand Down Expand Up @@ -68,18 +75,17 @@ let package = Package(
),

// test targets
.target(name: "TestSupport", dependencies: ["Core"]),
.testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]),
.testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]),
.testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]),
.testTarget(
name: "ReportingTests",
.target(
name: "TestSupport",
dependencies: [
"Core",
.product(name: "CustomDump", package: "swift-custom-dump"),
"Reporting",
"TestSupport",
]
),
.testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]),
.testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]),
.testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]),
.testTarget(name: "ReportingTests", dependencies: ["Reporting", "TestSupport"]),
.testTarget(name: "CommandsTests", dependencies: ["Commands"]),
]
)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ multiline2: |+
multiline3: |-
This will ignore any trailing newlines and
will end with the lest non-newline character (the following dot in this case -->).
will end with the last non-newline character (the following dot in this case -->).
```
Expand Down
8 changes: 4 additions & 4 deletions Sources/Checkers/FileContentsChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ extension FileContentsChecker: Checker {
let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(id)"#)

for match in regex.matches(in: fileContents).reversed() {
let fileLocation = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath)
let location = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath)

// skip found match if contains `AnyLint.skipHere: <CheckInfo.ID>` in same line or one line before
guard
!linesInFile.containsLine(at: [fileLocation.row! - 2, fileLocation.row! - 1], matchingRegex: skipHereRegex)
!linesInFile.containsLine(at: [location.row! - 2, location.row! - 1], matchingRegex: skipHereRegex)
else { continue }

let autoCorrection: AutoCorrection? = {
Expand All @@ -70,7 +70,7 @@ extension FileContentsChecker: Checker {
violations.append(
Violation(
matchedString: match.string,
fileLocation: fileLocation,
location: location,
appliedAutoCorrection: autoCorrection
)
)
Expand All @@ -93,7 +93,7 @@ extension FileContentsChecker: Checker {
if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) {
// only paths where auto-corrections were applied need to be re-checked
let filePathsToReCheck = Array(
Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.fileLocation!.filePath })
Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.location!.filePath })
)
.sorted()

Expand Down
4 changes: 2 additions & 2 deletions Sources/Checkers/FilePathsChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension FilePathsChecker: Checker {
let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count
if matchingFilePathsCount <= 0 {
violations.append(
Violation(fileLocation: nil, appliedAutoCorrection: nil)
Violation(location: nil, appliedAutoCorrection: nil)
)
}
}
Expand All @@ -50,7 +50,7 @@ extension FilePathsChecker: Checker {

violations.append(
Violation(
fileLocation: .init(filePath: filePath),
location: .init(filePath: filePath),
appliedAutoCorrection: appliedAutoCorrection
)
)
Expand Down
48 changes: 44 additions & 4 deletions Sources/Checkers/Lint.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Foundation
import Core
import OrderedCollections
import ShellOut
import Reporting

/// The linter type providing APIs for checking anything using regular expressions.
public enum Lint {
Expand Down Expand Up @@ -129,9 +131,27 @@ public enum Lint {

/// Run custom scripts as checks.
///
/// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. Else, it will report exactly one violation if the command has a non-zero exit code with the last line(s) of output.
public static func runCustomScript(checkInfo: CheckInfo, command: String) throws -> [Violation] {
fatalError() // TODO: [cg_2021-07-09] not yet implemented
/// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. If the output iis an array of ``Violation`` instances, they will be wrapped in a ``LintResults`` object. Else, it will report exactly one violation if the command has a non-zero exit code with the last line(s) of output.
public static func runCustomScript(checkInfo: CheckInfo, command: String) throws -> LintResults {
let tempScriptFileUrl = URL(fileURLWithPath: "_\(checkInfo.id).tempscript")
try command.write(to: tempScriptFileUrl, atomically: true, encoding: .utf8)

let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path])
if let jsonString = output.lintResultsJsonString,
let jsonData = jsonString.data(using: .utf8),
let lintResults: LintResults = try? JSONDecoder.iso.decode(LintResults.self, from: jsonData)
{
return lintResults
}
else if let jsonString = output.violationsArrayJsonString,
let jsonData = jsonString.data(using: .utf8),
let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData)
{
return [checkInfo.severity: [checkInfo: violations]]
}
else {
return [checkInfo.severity: [checkInfo: [Violation()]]]
}
}

static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) {
Expand Down Expand Up @@ -196,10 +216,30 @@ public enum Lint {

guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else {
log.message(
"Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.",
"Incompatible options specified for check \(checkInfo.id): `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together.",
level: .error
)
log.exit(fail: true)
}
}
}

fileprivate extension String {
var lintResultsJsonString: String? {
try! Regex(
#"\{.*(?:\"error\"\s*:|\"warning\"\s*:|\"info\"\s*:).*\}"#,
options: .dotMatchesLineSeparators
)
.firstMatch(in: self)?
.string
}

var violationsArrayJsonString: String? {
try! Regex(
#"\[(?:\s*\{.*\}\s*,)*(?:\s*\{.*\}\s*)?\]"#,
options: .dotMatchesLineSeparators
)
.firstMatch(in: self)?
.string
}
}
4 changes: 2 additions & 2 deletions Sources/Commands/LintCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ struct LintCommand: ParsableCommand {

// run `CustomScripts` checks
for customScriptConfig in lintConfig.customScripts {
let violations = try Lint.runCustomScript(
let customScriptLintResults = try Lint.runCustomScript(
checkInfo: customScriptConfig.checkInfo,
command: customScriptConfig.command
)

lintResults.appendViolations(violations, forCheck: customScriptConfig.checkInfo)
lintResults.mergeResults(customScriptLintResults)
}

// report violations & exit with right status code
Expand Down
2 changes: 1 addition & 1 deletion Sources/Configuration/Templates/Blank.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ CustomScripts: []
# if which yamllint > /dev/null; then
# yamllint anylint.yml
# else
# echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint"
# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }'
# fi
2 changes: 1 addition & 1 deletion Sources/Configuration/Templates/OpenSource.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ CustomScripts:
if which yamllint > /dev/null; then
yamllint anylint.yml
else
echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint"
echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }'
fi
2 changes: 1 addition & 1 deletion Sources/Core/AutoCorrection.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Information about an autocorrection.
public struct AutoCorrection: Codable {
public struct AutoCorrection: Codable, Equatable {
private enum Constants {
/// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs.
static let newlinesRequiredForDiffing: Int = 3
Expand Down
31 changes: 21 additions & 10 deletions Sources/Core/CheckInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,26 @@ extension CheckInfo: Codable {
) throws {
let container = try decoder.singleValueContainer()
let rawString = try container.decode(String.self)
self.init(rawValue: rawString)!
}

let customSeverityRegex = try Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#)
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}

extension CheckInfo: RawRepresentable {
public var rawValue: String {
"\(id)@\(severity.rawValue): \(hint)"
}

if let match = customSeverityRegex.firstMatch(in: rawString) {
public init?(
rawValue: String
) {
let customSeverityRegex = try! Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#)

if let match = customSeverityRegex.firstMatch(in: rawValue) {
let id = match.captures[0]!
let severityString = match.captures[1]!
let hint = match.captures[2]!
Expand All @@ -54,11 +70,11 @@ extension CheckInfo: Codable {
self = .init(id: id, hint: hint, severity: severity)
}
else {
let defaultSeverityRegex = try Regex(#"^([^@:]+): ?(.*$)"#)
let defaultSeverityRegex = try! Regex(#"^([^@:]+): ?(.*$)"#)

guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawString) else {
guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawValue) else {
log.message(
"Could not convert String literal '\(rawString)' to type CheckInfo. Please check the structure to be: <id>(@<severity>): <hint>",
"Could not convert String literal '\(rawValue)' to type CheckInfo. Please check the structure to be: <id>(@<severity>): <hint>",
level: .error
)
log.exit(fail: true)
Expand All @@ -70,9 +86,4 @@ extension CheckInfo: Codable {
self = .init(id: id, hint: hint)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("\(id)@\(severity.rawValue): \(hint)")
}
}
2 changes: 1 addition & 1 deletion Sources/Core/Location.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Info about the exact location of a character in a given file.
public struct Location: Codable {
public struct Location: Codable, Equatable {
/// The path to the file.
public let filePath: String

Expand Down
2 changes: 1 addition & 1 deletion Sources/Core/Logging/ConsoleLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public final class ConsoleLogger: Loggable {
/// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning.
/// - level: The level of the print statement.
/// - location: The file, line and char in line location string.
public func message(_ message: String, level: PrintLevel, fileLocation: Location?) {
public func message(_ message: String, level: PrintLevel, location: Location?) {
switch level {
case .success:
print(formattedCurrentTime(), "", message.green)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Core/Logging/Loggable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation
public var log: Loggable = ConsoleLogger()

public protocol Loggable {
func message(_ message: String, level: PrintLevel, fileLocation: Location?)
func message(_ message: String, level: PrintLevel, location: Location?)
}

extension Loggable {
Expand All @@ -21,6 +21,6 @@ extension Loggable {

/// Convenience overload of `message(:level:fileLocation:)` with `fileLocation` set to `nil`.
public func message(_ message: String, level: PrintLevel) {
self.message(message, level: level, fileLocation: nil)
self.message(message, level: level, location: nil)
}
}
6 changes: 3 additions & 3 deletions Sources/Core/Logging/XcodeLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ public final class XcodeLogger: Loggable {
/// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning.
/// - level: The level of the print statement.
/// - location: The file, line and char in line location string.
public func message(_ message: String, level: PrintLevel, fileLocation: Location?) {
public func message(_ message: String, level: PrintLevel, location: Location?) {
var locationPrefix = ""

if let fileLocation = fileLocation {
locationPrefix = fileLocation.locationMessage(pathType: .absolute) + " "
if let location = location {
locationPrefix = location.locationMessage(pathType: .absolute) + " "
}

print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)")
Expand Down
8 changes: 4 additions & 4 deletions Sources/Core/Violation.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import Foundation

/// A violation found in a check.
public struct Violation: Codable {
public struct Violation: Codable, Equatable {
/// The matched string that violates the check.
public let matchedString: String?

/// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified.
public let fileLocation: Location?
public let location: Location?

/// The autocorrection applied to fix this violation.
public let appliedAutoCorrection: AutoCorrection?

/// Initializes a violation object.
public init(
matchedString: String? = nil,
fileLocation: Location? = nil,
location: Location? = nil,
appliedAutoCorrection: AutoCorrection? = nil
) {
self.matchedString = matchedString
self.fileLocation = fileLocation
self.location = location
self.appliedAutoCorrection = appliedAutoCorrection
}
}
2 changes: 1 addition & 1 deletion Sources/Reporting/Extensions/JSONDecoderExt.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

extension JSONDecoder {
static var iso: JSONDecoder {
public static var iso: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
Expand Down
3 changes: 2 additions & 1 deletion Sources/Reporting/Extensions/JSONEncoderExt.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Foundation

extension JSONEncoder {
static var iso: JSONEncoder {
public static var iso: JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
return encoder
}
}
Loading

0 comments on commit e7cc711

Please sign in to comment.