Installation • Getting Started • Configuration • Donation • Issues • Contributing • License
Lint any project in any language using Swift and regular expressions. With built-in support for matching and non-matching examples validation & autocorrect replacement. Replaces SwiftLint custom rules & works for other languages as well! 🎉
Via Homebrew:
To install AnyLint the first time, run these commands:
brew tap Flinesoft/AnyLint https://github.com/Flinesoft/AnyLint.git
brew install anylint
To update it to the latest version, run this instead:
brew upgrade anylint
Via Mint:
To install AnyLint or update to the latest version, run this command:
mint install Flinesoft/AnyLint
To initialize AnyLint in a project, run:
anylint --init blank
This will create the Swift script file lint.swift
with something like the following contents:
#!/usr/local/bin/swift-sh
import AnyLint // @Flinesoft ~> 0.1.1
// MARK: - Variables
let readmeFile: Regex = #"README\.md"#
// MARK: - Checks
// MARK: Readme
try Lint.checkFilePaths(
checkInfo: "Readme: Each project should have a README.md file explaining the project.",
regex: readmeFile,
matchingExamples: ["README.md"],
nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"],
violateIfNoMatchesFound: true
)
// MARK: ReadmeTypoLicense
try Lint.checkFileContents(
checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.",
regex: #"([\s#]L|l)isence([\s\.,:;])"#,
matchingExamples: [" license:", "## Lisence\n"],
nonMatchingExamples: [" license:", "## License\n"],
includeFilters: [readmeFile],
autoCorrectReplacement: "$1icense$2",
autoCorrectExamples: [
AutoCorrection(before: " license:", after: " license:"),
AutoCorrection(before: "## Lisence\n", after: "## License\n"),
]
)
// MARK: - Log Summary & Exit
Lint.logSummaryAndExit()
The most important thing to note is that the first two lines and the last line are required for AnyLint to work properly.
Other than that, all the other code in between can be adjusted and that's actually where you configure your lint checks (a few examples are provided by default in the blank
template). Note that the first two lines declare the file to be a Swift script using swift-sh. Thus, you can run any Swift code and even import Swift packages (see the swift-sh docs) if you need to. The last line makes sure that all violations found in the process of running the previous code are reported properly and exits the script with the proper exit code.
Having this configuration file, you can now run anylint
to run your lint checks. By default, if any check fails, the entire command fails and reports the violation reason. To learn more about how to configure your own checks, see the Configuration section below.
If you want to create and run multiple configuration files or if you want a different name or location for the default config file, you can pass the --path
option, which can be used multiple times as well like this:
Initializes the configuration files at the given locations:
anylint --init blank --path Sources/lint.swift --path Tests/lint.swift
Runs the lint checks for both configuration files:
anylint --path Sources/lint.swift --path Tests/lint.swift
AnyLint provides three different kinds of lint checks:
checkFileContents
: Matches the contents of a text file to a given regex.checkFilePaths
: Matches the file paths of the current directory to a given regex.customCheck
: Allows to write custom Swift code to do other kinds of checks.
Several examples of lint checks can be found in the lint.swift
file of this very project.
Independent from the method used, there are a few types specified in the AnyLint package you should know of.
Many parameters in the above mentioned lint check methods are of Regex
type. A Regex
can be initialized in several ways:
- Using a String:
let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/
- Using a String Literal:
let regex: Regex = #"(foo|bar)[0-9]+"# // => /(foo|bar)[0-9]+/
- Using a Dictionary Literal: (use for named capture groups)
let regex: Regex = ["key": #"foo|bar"#, "num": "[0-9]+"]
// => /(?<key>foo|bar)(?<num>[0-9]+)/
Note that we recommend using raw strings (#"foo"#
instead of "foo"
) for all regexes to get rid of double escaping backslashes (e.g. \\s
becomes \s
). This also allows for testing regexes in online regex editors like rubular first and then copy & pasting from them without any additional escaping.
A CheckInfo
contains the basic information about a lint check. It consists of:
id
: The identifier of your lint check. For example:EmptyTodo
hint
: The hint explaining the cause of the violation or the steps to fix it.severity
: The severity of violations. One oferror
,warning
,info
. Default:error
While there is an initializer available, we recommend using a String Literal instead like so:
// 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`."
AnyLint has rich support for checking the contents of a file using a regex. The design follows the approach "make simple things simple and hard things possible". Thus, let's explain the checkFileContents
method with a simple and a complex example.
In its simplest form, the method just requires a checkInfo
and a regex
:
// MARK: EmptyTodo
try Lint.checkFileContents(
checkInfo: "EmptyTodo: TODO comments should not be empty.",
regex: #"// TODO: *\n"#
)
But we strongly recommend to always provide also:
matchingExamples
: Array of strings expected to match the given string forregex
validation.nonMatchingExamples
: Array of strings not matching the given string forregex
validation.includeFilters
: Array ofRegex
objects to include to the file paths to check.
The first two will be used on each run of AnyLint to check if the provided regex
actually works as expected. If any of the matchingExamples
doesn't match or if any of the nonMatchingExamples
does match, the entire AnyLint command will fail early. This a built-in validation step to help preventing a lot of issues and increasing your confidence on the lint checks.
The third one is recommended because it increases the performance of the linter. Only files at paths matching at least one of the provided regexes will be checked. If not provided, all files within the current directory will be read recursively for each check, which is inefficient.
Here's the recommended minimum example:
// MARK: - Variables
let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
let swiftTestFiles: Regex = #"Tests/.*\.swift"#
// MARK: - Checks
// MARK: empty_todo
try Lint.checkFileContents(
checkInfo: "EmptyTodo: TODO comments should not be empty.",
regex: #"// TODO: *\n"#,
matchingExamples: ["// TODO:\n"],
nonMatchingExamples: ["// TODO: not yet implemented\n"],
includeFilters: [swiftSourceFiles, swiftTestFiles]
)
There's 3 more parameters you can optionally set if needed:
excludeFilters
: Array ofRegex
objects to exclude from the file paths to check.autoCorrectReplacement
: Replacement string which can reference any capture groups in theregex
.autoCorrectExamples
: Example structs withbefore
andafter
for autocorrection validation.
The excludeFilters
can be used alternatively to the includeFilters
or alongside them. If used alongside, exclusion will take precedence over inclusion.
If autoCorrectReplacement
is provided, AnyLint will automatically replace matches of regex
with the given replacement string. Capture groups are supported, both in numbered style (([a-z]+)(\d+)
=> $1$2
) and named group style ((?<alpha>[a-z])(?<num>\d+)
=> $alpha$num
). When provided, we strongly recommend to also provide autoCorrectExamples
for validation. Like for matchingExamples
/ nonMatchingExamples
the entire command will fail early if one of the examples doesn't correct from the before
string to the expected after
string.
Caution: When using the
autoCorrectReplacement
parameter, be sure to double-check that your regex doesn't match too much content. Additionally, we strongly recommend to commit your changes regularly to have some backup.
Here's a full example using all parameters at once:
// MARK: - Variables
let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
let swiftTestFiles: Regex = #"Tests/.*\.swift"#
// MARK: - Checks
// MARK: empty_method_body
try Lint.checkFileContents(
checkInfo: "EmptyMethodBody: Don't use whitespaces for the body of empty methods.",
regex: [
"declaration": #"func [^\(\s]+\([^{]*\)"#,
"spacing": #"\s*"#,
"body": #"\{\s+\}"#
],
matchingExamples: [
"func foo2bar() { }",
"func foo2bar(x: Int, y: Int) { }",
"func foo2bar(\n x: Int,\n y: Int\n) {\n \n}",
],
nonMatchingExamples: [
"func foo2bar() {}",
"func foo2bar(x: Int, y: Int) {}"
],
includeFilters: [swiftSourceFiles],
excludeFilters: [swiftTestFiles],
autoCorrectReplacement: "$declaration {}",
autoCorrectExamples: [
AutoCorrection(before: "func foo2bar() { }", after: "func foo2bar() {}"),
AutoCorrection(before: "func foo2bar(x: Int, y: Int) { }", after: "func foo2bar(x: Int, y: Int) {}"),
AutoCorrection(before: "func foo2bar()\n{\n \n}", after: "func foo2bar() {}"),
]
)
The checkFilePaths
method has all the same parameters like the checkFileContents
method, so please read the above section to learn more about them. There's only one difference and one additional parameter:
autoCorrectReplacement
: Here, this will safely move the file using the path replacement.violateIfNoMatchesFound
: Will report a violation if no matches are found iftrue
. Default:false
As this method is about file paths and not file contents, the autoCorrectReplacement
actually also fixes the paths, which corresponds to moving files from the before
state to the after
state. Note that moving/renaming files here is done safely, which means that if a file already exists at the resulting path, the command will fail.
By default, checkFilePaths
will fail if the given regex
matches a file. If you want to check for the existence of a file though, you can set violateIfNoMatchesFound
to true
instead, then the method will fail if it does not matchn any file.
AnyLint allows you to do any kind of lint checks (thus its name) as it gives you the full power of the Swift programming language and it's packages ecosystem. The customCheck
method needs to be used to profit from this flexibility. And it's actually the simplest of the three methods, consisting of only two parameters:
checkInfo
: Provides some general information on the lint check.customClosure
: Your custom logic which produces an array ofViolation
objects.
Note that the Violation
type just holds some additional information on the file, matched string, location in the file and applied autocorrection and that all these fields are optional. It is a simple struct used by the AnyLint reporter for more detailed output, no logic attached. The only required field is the CheckInfo
object which caused the violation.
If you want to use regexes in your custom code, you can learn more about how you can match strings with a Regex
object on the HandySwift docs (the project, the class was taken from) or read the code documentation comments.
When using the customCheck
, you might want to also include some Swift packages for easier file handling or running shell commands. You can do so by adding them at the top of the file like so:
TODO: Improve the below code example with something more useful & realistic.
#!/usr/local/bin/swift-sh
import AnyLint // @Flinesoft ~> 0.1.1
import Files // @JohnSundell ~> 4.1.1
import ShellOut // @JohnSundell ~> 2.3.0
// MARK: echo
try Lint.customCheck(checkInfo: "Echo: Always say hello to the world.") {
var violations: [Violation] = []
// use ShellOut package
let output = try shellOut(to: "echo", arguments: ["Hello world"])
// ...
// use Files package
try Folder(path: "MyFolder").files.forEach { file in
// ...
}
return violations
}
// MARK: - Log Summary & Exit
Lint.logSummaryAndExit()
AnyLint was brought to you by Cihat Gündüz in his free time. If you want to thank me and support the development of this project, please make a small donation on PayPal. In case you also like my other open source contributions and articles, please consider motivating me by becoming a sponsor on GitHub or a patron on Patreon.
Thank you very much for any donation, it really helps out a lot! 💯
Contributions are welcome. Feel free to open an issue on GitHub with your ideas or implement an idea yourself and post a pull request. If you want to contribute code, please try to follow the same syntax and semantic in your commit messages (see rationale here). Also, please make sure to add an entry to the CHANGELOG.md
file which explains your change.
This library is released under the MIT License. See LICENSE for details.