Skip to content

Commit

Permalink
Merge branch 'release/0.2.0' into versions
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeehut committed Apr 10, 2020
2 parents e80ac90 + d6997ca commit c5fd1ad
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 80 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se
### Security
- None.

## [0.2.0] - 2020-04-10
### Added
- Added new `-x` / `--xcode` option to print out warnings & errors in an Xcode-compatible manner to improve user experience when used with an Xcode build script. Requires `arguments: CommandLine.arguments` as parameters to `logSummary` in config file.
Issue: [#4](https://github.com/Flinesoft/AnyLint/issues/4) | PR: [#8](https://github.com/Flinesoft/AnyLint/pull/8) | Author: [Cihat Gündüz](https://github.com/Jeehut)

## [0.1.1] - 2020-03-23
### Added
- Added two simple lint check examples in first code sample in README. (Thanks for the pointer, [Dave Verwer](https://github.com/daveverwer)!)
- Added two simple lint check examples in first code sample in README. (Thanks for the pointer, [Dave Verwer](https://github.com/daveverwer)!)
Author: [Cihat Gündüz](https://github.com/Jeehut)
### Changed
- Changed `CheckInfo` id casing convention from snake_case to UpperCamelCase in `blank` template.
Expand Down
2 changes: 1 addition & 1 deletion Formula/anylint.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Anylint < Formula
desc "Lint anything by combining the power of Swift & regular expressions"
homepage "https://github.com/Flinesoft/AnyLint"
url "https://github.com/Flinesoft/AnyLint.git", :tag => "0.1.0", :revision => "25fec7dd29d86f0ef97fc2dddcd41d2576d9570c"
url "https://github.com/Flinesoft/AnyLint.git", :tag => "0.1.1", :revision => "e80ac907d160a0e8f359dc84fabfbd1cc80a8b50"
head "https://github.com/Flinesoft/AnyLint.git"

depends_on :xcode => ["11.3", :build]
Expand Down
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Flinesoft/AnyLint/main/Logo.png"
width=562 height=115>
width=562 />
</p>

<p align="center">
Expand All @@ -17,8 +17,8 @@
alt="Coverage"/>
</a>
<a href="https://github.com/Flinesoft/AnyLint/releases">
<img src="https://img.shields.io/badge/Version-0.1.1-blue.svg"
alt="Version: 0.1.1">
<img src="https://img.shields.io/badge/Version-0.2.0-blue.svg"
alt="Version: 0.2.0">
</a>
<a href="https://github.com/Flinesoft/AnyLint/blob/main/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-lightgrey.svg"
Expand All @@ -43,6 +43,7 @@
<a href="#installation">Installation</a>
• <a href="#getting-started">Getting Started</a>
• <a href="#configuration">Configuration</a>
• <a href="#xcode-build-script">Xcode Build Script</a>
• <a href="#donation">Donation</a>
• <a href="https://github.com/Flinesoft/AnyLint/issues">Issues</a>
• <a href="#contributing">Contributing</a>
Expand Down Expand Up @@ -86,11 +87,11 @@ To initialize AnyLint in a project, run:
anylint --init blank
```

This will create the Swift script file `lint.swift` with something like following contents:
This will create the Swift script file `lint.swift` with something like the following contents:

```swift
#!/usr/local/bin/swift-sh
import AnyLint // @Flinesoft ~> 0.1.1
import AnyLint // @Flinesoft ~> 0.2.0

// MARK: - Variables
let readmeFile: Regex = #"README\.md"#
Expand Down Expand Up @@ -120,7 +121,7 @@ try Lint.checkFileContents(
)

// MARK: - Log Summary & Exit
Lint.logSummaryAndExit()
Lint.logSummaryAndExit(arguments: CommandLine.arguments)
```

The most important thing to note is that the **first two lines and the last line are required** for AnyLint to work properly.
Expand Down Expand Up @@ -161,7 +162,7 @@ Many parameters in the above mentioned lint check methods are of `Regex` type. A

1. Using a **String**:
```swift
let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/`
let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/
```
2. Using a **String Literal**:
```swift
Expand All @@ -188,6 +189,7 @@ While there is an initializer available, we recommend using a String Literal ins
```swift
// accepted structure: <id>(@<severity>): <hint>
let checkInfo: CheckInfo = "ReadmePath: The README file should be named exactly `README.md`."
let checkInfoCustomSeverity: CheckInfo = "ReadmePath@warning: The README file should be named exactly `README.md`."
```

### Check File Contents
Expand All @@ -197,7 +199,7 @@ AnyLint has rich support for checking the contents of a file using a regex. The
In its simplest form, the method just requires a `checkInfo` and a `regex`:

```swift
// MARK: empty_todo
// MARK: EmptyTodo
try Lint.checkFileContents(
checkInfo: "EmptyTodo: TODO comments should not be empty.",
regex: #"// TODO: *\n"#
Expand Down Expand Up @@ -308,7 +310,7 @@ When using the `customCheck`, you might want to also include some Swift packages
```swift
#!/usr/local/bin/swift-sh
import AnyLint // @Flinesoft ~> 0.1.1
import AnyLint // @Flinesoft ~> 0.2.0
import Files // @JohnSundell ~> 4.1.1
import ShellOut // @JohnSundell ~> 2.3.0

Expand All @@ -329,9 +331,23 @@ try Lint.customCheck(checkInfo: "Echo: Always say hello to the world.") {
}

// MARK: - Log Summary & Exit
Lint.logSummaryAndExit()
Lint.logSummaryAndExit(arguments: CommandLine.arguments)
```

## Xcode Build Script

If you are using AnyLint for a project in Xcode, you can configure a build script to run it on each build. In order to do this select your target, choose the `Build Phases` tab and click the + button on the top left corner of that pane. Select `New Run Script Phase` and copy the following into the text box below the `Shell: /bin/sh` of your new run script phase:

```shell
if which anylint > /dev/null; then
anylint -x
else
echo "warning: AnyLint not installed, download it from https://github.com/Flinesoft/AnyLint"
fi
```

Next, make sure the AnyLint script runs before the steps `Compiling Sources` by moving it per drag & drop, for example right after `Dependencies`. You probably also want to rename it to somethng like `AnyLint`.

## Donation

AnyLint was brought to you by [Cihat Gündüz](https://github.com/Jeehut) in his free time. If you want to thank me and support the development of this project, please **make a small donation on [PayPal](https://paypal.me/Dschee/5EUR)**. In case you also like my other [open source contributions](https://github.com/Flinesoft) and [articles](https://medium.com/@Jeehut), please consider motivating me by **becoming a sponsor on [GitHub](https://github.com/sponsors/Jeehut)** or a **patron on [Patreon](https://www.patreon.com/Jeehut)**.
Expand Down
11 changes: 8 additions & 3 deletions Sources/AnyLint/Lint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,17 @@ public enum Lint {
}

/// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations.
public static func logSummaryAndExit(failOnWarnings: Bool = false) {
public static func logSummaryAndExit(failOnWarnings: Bool = false, arguments: [String] = []) {
let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue)
if targetIsXcode {
log = Logger(outputType: .xcode)
}

Statistics.shared.logSummary()

if Statistics.shared.violationsBySeverity[.error]!.isFilled {
if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled {
log.exit(status: .failure)
} else if failOnWarnings && Statistics.shared.violationsBySeverity[.warning]!.isFilled {
} else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled {
log.exit(status: .failure)
} else {
log.exit(status: .success)
Expand Down
6 changes: 6 additions & 0 deletions Sources/AnyLint/Severity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ public enum Severity: Int, CaseIterable {
}
}
}

extension Severity: Comparable {
public static func < (lhs: Severity, rhs: Severity) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
114 changes: 72 additions & 42 deletions Sources/AnyLint/Statistics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,55 +31,85 @@ final class Statistics {
if executedChecks.isEmpty {
log.message("No checks found to perform.", level: .warning)
} else if violationsBySeverity.values.contains(where: { $0.isFilled }) {
for check in executedChecks {
if let checkViolations = violationsPerCheck[check], checkViolations.isFilled {
let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage() != nil }

if violationsWithLocationMessage.isFilled {
log.message(
"\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:",
level: check.severity.logLevel
)
let numerationDigits = String(violationsWithLocationMessage.count).count

for (index, violation) in violationsWithLocationMessage.enumerated() {
let violationNumString = String(format: "%0\(numerationDigits)d", index + 1)
let prefix = "> \(violationNumString). "
log.message(prefix + violation.locationMessage()!, level: check.severity.logLevel)

let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined()
if let appliedAutoCorrection = violation.appliedAutoCorrection {
for messageLine in appliedAutoCorrection.appliedMessageLines {
log.message(prefixLengthWhitespaces + messageLine, level: .info)
}
} else if let matchedString = violation.matchedString {
log.message(prefixLengthWhitespaces + "Matching string:".bold + " (trimmed & reduced whitespaces)", level: .info)
let matchedStringOutput = matchedString
.showNewlines()
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: " ", with: " ")
.replacingOccurrences(of: " ", with: " ")
.replacingOccurrences(of: " ", with: " ")
log.message(prefixLengthWhitespaces + "> " + matchedStringOutput, level: .info)
switch log.outputType {
case .console, .test:
logViolationsToConsole()

case .xcode:
showViolationsInXcode()
}
} else {
log.message("Performed \(executedChecks.count) check(s) without any violations.", level: .success)
}
}

func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] {
let violations: [Violation] = violationsBySeverity[severity]!
guard excludeAutocorrected else { return violations }
return violations.filter { $0.appliedAutoCorrection == nil }
}

private func logViolationsToConsole() {
for check in executedChecks {
if let checkViolations = violationsPerCheck[check], checkViolations.isFilled {
let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage(pathType: .relative) != nil }

if violationsWithLocationMessage.isFilled {
log.message(
"\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:",
level: check.severity.logLevel
)
let numerationDigits = String(violationsWithLocationMessage.count).count

for (index, violation) in violationsWithLocationMessage.enumerated() {
let violationNumString = String(format: "%0\(numerationDigits)d", index + 1)
let prefix = "> \(violationNumString). "
log.message(prefix + violation.locationMessage(pathType: .relative)!, level: check.severity.logLevel)

let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined()
if let appliedAutoCorrection = violation.appliedAutoCorrection {
for messageLine in appliedAutoCorrection.appliedMessageLines {
log.message(prefixLengthWhitespaces + messageLine, level: .info)
}
} else if let matchedString = violation.matchedString {
log.message(prefixLengthWhitespaces + "Matching string:".bold + " (trimmed & reduced whitespaces)", level: .info)
let matchedStringOutput = matchedString
.showNewlines()
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: " ", with: " ")
.replacingOccurrences(of: " ", with: " ")
.replacingOccurrences(of: " ", with: " ")
log.message(prefixLengthWhitespaces + "> " + matchedStringOutput, level: .info)
}
} else {
log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel)
}

log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.logLevel)
} else {
log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel)
}

log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.logLevel)
}
}

let errors = "\(violationsBySeverity[.error]!.count) error(s)"
let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)"
let errors = "\(violationsBySeverity[.error]!.count) error(s)"
let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)"

log.message(
"Performed \(executedChecks.count) check(s) and found \(errors) & \(warnings).",
level: maxViolationSeverity!.logLevel
)
} else {
log.message("Performed \(executedChecks.count) check(s) without any violations.", level: .success)
log.message(
"Performed \(executedChecks.count) check(s) and found \(errors) & \(warnings).",
level: maxViolationSeverity!.logLevel
)
}

private func showViolationsInXcode() {
for severity in violationsBySeverity.keys.sorted().reversed() {
let severityViolations = violationsBySeverity[severity]!
for violation in severityViolations where violation.appliedAutoCorrection == nil {
let check = violation.checkInfo
log.xcodeMessage(
"[\(check.id)] \(check.hint)",
level: check.severity.logLevel,
location: violation.locationMessage(pathType: .absolute)
)
}
}
}
}
6 changes: 3 additions & 3 deletions Sources/AnyLint/Violation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public struct Violation {
self.appliedAutoCorrection = appliedAutoCorrection
}

func locationMessage() -> String? {
func locationMessage(pathType: String.PathType) -> String? {
guard let filePath = filePath else { return nil }
guard let locationInfo = locationInfo else { return filePath }
return "\(filePath):\(locationInfo.line):\(locationInfo.charInLine)"
guard let locationInfo = locationInfo else { return filePath.path(type: pathType) }
return "\(filePath.path(type: pathType)):\(locationInfo.line):\(locationInfo.charInLine):"
}
}
7 changes: 7 additions & 0 deletions Sources/AnyLintCLI/Commands/SingleCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class SingleCommand: Command {
@Flag("-v", "--version", description: "Print the current tool version")
var version: Bool

@Flag("-x", "--xcode", description: "Print warnings & errors in a format to be reported right within Xcodes left sidebar")
var xcode: Bool

@Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]")
var initTemplateName: String?

Expand All @@ -20,6 +23,10 @@ class SingleCommand: Command {

// MARK: - Execution
func execute() throws {
if xcode {
log = Logger(outputType: .xcode)
}

// version subcommand
if version {
try VersionTask().perform()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ extension ConfigurationTemplate {
}

static var commonSuffix: String {
"\n\n// MARK: - Log Summary & Exit\nLint.logSummaryAndExit()\n"
"\n\n// MARK: - Log Summary & Exit\nLint.logSummaryAndExit(arguments: CommandLine.arguments)\n"
}
}
7 changes: 5 additions & 2 deletions Sources/AnyLintCLI/Tasks/LintTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ extension LintTask: TaskHandler {

do {
log.message("Start linting using config file at \(configFilePath) ...", level: .info)
try Task.run(bash: "\(configFilePath.absolutePath)")
try Task.run(bash: "\(configFilePath.absolutePath) \(log.outputType.rawValue)")
log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success)
} catch is RunError {
log.message("Linting failed using config file at \(configFilePath).", level: .error)
if log.outputType != .xcode {
log.message("Linting failed using config file at \(configFilePath).", level: .error)
}

throw LintError.configFileFailed
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Utility/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public var log = Logger(outputType: .console)
/// Constants to reference across the project.
public enum Constants {
/// The current tool version string. Conforms to SemVer 2.0.
public static let currentVersion: String = "0.1.1"
public static let currentVersion: String = "0.2.0"

/// The name of this tool.
public static let toolName: String = "AnyLint"
Expand Down
Loading

0 comments on commit c5fd1ad

Please sign in to comment.