Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Adds a template system for local, remote & github configs #37

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
path: anylint-cache
key: ${{ runner.os }}-v1-anylint-${{ env.ANYLINT_LATEST_VERSION }}-swift-sh-${{ env.SWIFT_SH_LATEST_VERSION }}

- name: Copy from cache
- name: Copy from Cache
if: steps.anylint-cache.outputs.cache-hit
run: |
sudo cp -f anylint-cache/anylint /usr/local/bin/anylint
Expand All @@ -59,7 +59,7 @@ jobs:
swift build -c release
sudo cp -f .build/release/swift-sh /usr/local/bin/swift-sh

- name: Copy to cache
- name: Copy to Cache
if: steps.anylint-cache.outputs.cache-hit != 'true'
run: |
mkdir -p anylint-cache
Expand Down Expand Up @@ -89,16 +89,62 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Export latest tool versions
run: |
latest_version() {
curl --silent "https://api.github.com/repos/$1/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}
echo "::set-env name=ANYLINT_LATEST_VERSION::$( latest_version Flinesoft/AnyLint )"
echo "::set-env name=SWIFT_SH_LATEST_VERSION::$( latest_version mxcl/swift-sh )"

- name: AnyLint Cache
uses: actions/cache@v1
id: anylint-cache
with:
path: anylint-cache
key: ${{ runner.os }}-v1-anylint-${{ env.ANYLINT_LATEST_VERSION }}-swift-sh-${{ env.SWIFT_SH_LATEST_VERSION }}

- name: Copy from Cache
if: steps.anylint-cache.outputs.cache-hit
run: |
sudo cp -f anylint-cache/anylint /usr/local/bin/anylint
sudo cp -f anylint-cache/swift-sh /usr/local/bin/swift-sh

- name: Install AnyLint
if: steps.anylint-cache.outputs.cache-hit != 'true'
run: |
git clone https://github.com/Flinesoft/AnyLint.git
cd AnyLint
swift build -c release
sudo cp -f .build/release/anylint /usr/local/bin/anylint

- name: Install swift-sh
if: steps.anylint-cache.outputs.cache-hit != 'true'
run: |
git clone https://github.com/mxcl/swift-sh.git
cd swift-sh
swift build -c release
sudo cp -f .build/release/swift-sh /usr/local/bin/swift-sh

- name: Copy to Cache
if: steps.anylint-cache.outputs.cache-hit != 'true'
run: |
mkdir -p anylint-cache
cp -f /usr/local/bin/anylint anylint-cache/anylint
cp -f /usr/local/bin/swift-sh anylint-cache/swift-sh

- name: Run tests
run: swift test -v


test-macos:
runs-on: macos-latest

steps:
- uses: actions/checkout@v2

- name: Install Swift-SH
run: brew install swift-sh

- name: Run tests
run: swift test -v --enable-code-coverage

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ xcuserdata/
.codacy-coverage
*.lcov
codacy-coverage.json
.anylint
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/jakeheis/SwiftCLI.git",
"state": {
"branch": null,
"revision": "c72c4564f8c0a24700a59824880536aca45a4cae",
"version": "6.0.1"
"revision": "2816678bcc37f4833d32abeddbdf5e757fa891d8",
"version": "6.0.2"
}
}
]
Expand Down
10 changes: 3 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,19 @@ let package = Package(
targets: [
.target(
name: "AnyLint",
dependencies: ["Utility"]
dependencies: ["SwiftCLI", "Utility"]
),
.testTarget(
name: "AnyLintTests",
dependencies: ["AnyLint"]
dependencies: ["AnyLint", "Rainbow", "SwiftCLI"]
),
.target(
name: "AnyLintCLI",
dependencies: ["Rainbow", "SwiftCLI", "Utility"]
),
.testTarget(
name: "AnyLintCLITests",
dependencies: ["AnyLintCLI"]
),
.target(
name: "Utility",
dependencies: ["Rainbow"]
dependencies: ["Rainbow", "SwiftCLI"]
),
.testTarget(
name: "UtilityTests",
Expand Down
3 changes: 2 additions & 1 deletion Sources/AnyLint/AutoCorrection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ extension AutoCorrection: ExpressibleByDictionaryLiteral {
}
}

// TODO: make the autocorrection diff sorted by line number
extension AutoCorrection: Codable {}

@available(OSX 10.15, *)
extension CollectionDifference.Change: Comparable where ChangeElement == String {
public static func < (lhs: Self, rhs: Self) -> Bool {
Expand Down
2 changes: 2 additions & 0 deletions Sources/AnyLint/CheckInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ extension CheckInfo: ExpressibleByStringLiteral {
}
}
}

extension CheckInfo: Codable {}
2 changes: 1 addition & 1 deletion Sources/AnyLint/Checkers/Checker.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation

protocol Checker {
func performCheck() throws -> [Violation]
func performCheck() throws -> [CheckInfo: [Violation]]
}
12 changes: 7 additions & 5 deletions Sources/AnyLint/Checkers/FileContentsChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct FileContentsChecker {
}

extension FileContentsChecker: Checker {
func performCheck() throws -> [Violation] { // swiftlint:disable:this function_body_length
func performCheck() throws -> [CheckInfo: [Violation]] { // swiftlint:disable:this function_body_length
log.message("Start checking \(checkInfo) ...", level: .debug)
var violations: [Violation] = []

Expand Down Expand Up @@ -82,7 +82,7 @@ extension FileContentsChecker: Checker {
)
}

Statistics.shared.checkedFiles(at: [filePath])
Statistics.default.checkedFiles(at: [filePath])
}

violations = violations.reversed()
Expand All @@ -91,7 +91,9 @@ extension FileContentsChecker: Checker {
log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug)

// only paths where auto-corrections were applied need to be re-checked
let filePathsToReCheck = Array(Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })).sorted()
let filePathsToReCheck = Array(
Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })
).sorted()

let violationsOnRechecks = try FileContentsChecker(
checkInfo: checkInfo,
Expand All @@ -100,9 +102,9 @@ extension FileContentsChecker: Checker {
autoCorrectReplacement: autoCorrectReplacement,
repeatIfAutoCorrected: repeatIfAutoCorrected
).performCheck()
violations.append(contentsOf: violationsOnRechecks)
violations.append(contentsOf: violationsOnRechecks[checkInfo]!)
}

return violations
return [checkInfo: violations]
}
}
6 changes: 3 additions & 3 deletions Sources/AnyLint/Checkers/FilePathsChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct FilePathsChecker {
}

extension FilePathsChecker: Checker {
func performCheck() throws -> [Violation] {
func performCheck() throws -> [CheckInfo: [Violation]] {
var violations: [Violation] = []

if violateIfNoMatchesFound {
Expand Down Expand Up @@ -44,9 +44,9 @@ extension FilePathsChecker: Checker {
)
}

Statistics.shared.checkedFiles(at: filePathsToCheck)
Statistics.default.checkedFiles(at: Set(filePathsToCheck))
}

return violations
return [checkInfo: violations]
}
}
102 changes: 102 additions & 0 deletions Sources/AnyLint/Checkers/TemplateChecker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import SwiftCLI
import Utility

/// The source of the subchecks to run.
public enum CheckSource {
/// The device-local source, requiring a path String.
case local(String)

/// A remote public URL source, requiring the full config file URL string.
case remote(String)

/// A GitHub repo source config file specified via repo (e.g. 'Flinesoft/AnyLint-Swift'), version (tag or branch) and variant (a subpath to the config file).
case github(repo: String, version: String, variant: String)
}

struct TemplateChecker {
let source: CheckSource
let runOnly: [String]?
let exclude: [String]?
let logDebugLevel: Bool
}

extension TemplateChecker: Checker {
func performCheck() throws -> [CheckInfo: [Violation]] {
var correctedSource: CheckSource = source

if let remoteSource = convertGitHubToRemoteSource(source: correctedSource) {
correctedSource = remoteSource
}

if let localSource = try downloadRemoteSourceToLocal(source: correctedSource) {
correctedSource = localSource
}

guard case let .local(templateFilePath) = correctedSource else {
log.message("Found unexpected state while validating checks source.", level: .error)
log.exit(status: .failure)
return [:] // only reachable in unit tests
}

if !fileManager.isExecutableFile(atPath: templateFilePath) {
try Task.run(bash: "chmod +x '\(templateFilePath)'")
}

log.message("Running local config file at '\(templateFilePath)'", level: .info)

var command = "anylint --path \(templateFilePath.absolutePath)"
if logDebugLevel {
command += " \(Constants.debugArgument)"
}
try Task.run(bash: command)

let dumpFileUrl = URL(fileURLWithPath: Constants.statisticsDumpFilePath)

guard
let dumpFileData = try? Data(contentsOf: dumpFileUrl),
let dumpedStatistics = try? JSONDecoder().decode(Statistics.self, from: dumpFileData)
else {
log.message("Could not decode Statistics JSON at \(dumpFileUrl.path)", level: .error)
log.exit(status: .failure)
return [:] // only reachable in unit tests
}

try fileManager.removeItem(atPath: Constants.statisticsDumpFilePath)
return dumpedStatistics.violationsPerCheck
}

private func convertGitHubToRemoteSource(source: CheckSource) -> CheckSource? {
guard case let .github(repo, version, variant) = source else { return nil }
log.message("Converting .github source to .remote source ...", level: .debug)
return .remote("https://raw.githubusercontent.com/\(repo)/\(version)/\(variant).swift")
}

private func downloadRemoteSourceToLocal(source: CheckSource) throws -> CheckSource? {
guard case let .remote(urlString) = source else { return nil }

log.message("Downloading .remote source from '\(urlString)' ...", level: .debug)
guard let remoteUrl = URL(string: urlString) else {
log.message("`.remote` source URL string '\(urlString)' is not a valid URL.", level: .error)
log.exit(status: .failure)
return nil // only reachable in unit tests
}

let remoteFileContents = try String(contentsOf: remoteUrl)
let uniqueFileName = (
remoteUrl.pathComponents.dropFirst().prefix(2) + remoteUrl.deletingPathExtension().pathComponents.suffix(2)
).joined(separator: "_")
let localFilePath = "\(Constants.tempDirPath)/\(uniqueFileName).swift"

if !fileManager.fileExists(atPath: Constants.tempDirPath) {
try fileManager.createDirectory(atPath: Constants.tempDirPath, withIntermediateDirectories: true, attributes: nil)
}

try remoteFileContents.write(toFile: localFilePath, atomically: true, encoding: .utf8)

return .local(localFilePath)
}
}
9 changes: 6 additions & 3 deletions Sources/AnyLint/Extensions/StringExt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ public typealias Regex = Utility.Regex

extension String {
/// Info about the exact location of a character in a given file.
public typealias LocationInfo = (line: Int, charInLine: Int)
public struct LocationInfo: Codable {
let line: Int
let charInLine: Int
}

/// Returns the location info for a given line index.
public func locationInfo(of index: String.Index) -> LocationInfo {
let prefix = self[startIndex ..< index]
let prefixLines = prefix.components(separatedBy: .newlines)
guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) }
guard let lastPrefixLine = prefixLines.last else { return LocationInfo(line: 1, charInLine: 1) }

let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1
return (line: prefixLines.count, charInLine: charInLine)
return LocationInfo(line: prefixLines.count, charInLine: charInLine)
}

func showNewlines() -> String {
Expand Down
Loading