= {
215 | Range(self.result.range, in: self.baseString)!
216 | }()
217 |
218 | /// The matching string for each capture group in the regular expression
219 | /// (if any).
220 | ///
221 | /// **Note:** Usually if the match was successful, the captures will by
222 | /// definition be non-nil. However if a given capture group is optional, the
223 | /// captured string may also be nil, depending on the particular string that
224 | /// is being matched against.
225 | ///
226 | /// Example:
227 | ///
228 | /// let regex = Regex("(a)?(b)")
229 | ///
230 | /// regex.matches(in: "ab")first?.captures // [Optional("a"), Optional("b")]
231 | /// regex.matches(in: "b").first?.captures // [nil, Optional("b")]
232 | public lazy var captures: [String?] = {
233 | let captureRanges = stride(from: 0, to: result.numberOfRanges, by: 1)
234 | .map(result.range)
235 | .dropFirst()
236 | .map { [unowned self] in
237 | Range($0, in: self.baseString)
238 | }
239 |
240 | return captureRanges.map { [unowned self] captureRange in
241 | guard let captureRange = captureRange else { return nil }
242 | return String(describing: self.baseString[captureRange])
243 | }
244 | }()
245 |
246 | let result: NSTextCheckingResult
247 |
248 | let baseString: String
249 |
250 | // MARK: - Initializers
251 | internal init(result: NSTextCheckingResult, in string: String) {
252 | precondition(
253 | result.regularExpression != nil,
254 | "NSTextCheckingResult must originate from regular expression parsing."
255 | )
256 |
257 | self.result = result
258 | self.baseString = string
259 | }
260 |
261 | // MARK: - Methods
262 | /// Returns a new string where the matched string is replaced according to the `template`.
263 | ///
264 | /// The template string may be a literal string, or include template variables:
265 | /// the variable `$0` will be replaced with the entire matched substring, `$1`
266 | /// with the first capture group, etc.
267 | ///
268 | /// For example, to include the literal string "$1" in the replacement string,
269 | /// you must escape the "$": `\$1`.
270 | ///
271 | /// - parameters:
272 | /// - template: The template string used to replace matches.
273 | ///
274 | /// - returns: A string with `template` applied to the matched string.
275 | public func string(applyingTemplate template: String) -> String {
276 | let replacement = result.regularExpression!.replacementString(
277 | for: result,
278 | in: baseString,
279 | offset: 0,
280 | template: template
281 | )
282 |
283 | return replacement
284 | }
285 |
286 | // MARK: - CustomStringConvertible
287 | /// Returns a string describing the match.
288 | public var description: String {
289 | "Match<\"\(string)\">"
290 | }
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/Sources/AnyLint/Lint.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Utility
3 |
4 | /// The linter type providing APIs for checking anything using regular expressions.
5 | public enum Lint {
6 | /// Checks the contents of files.
7 | ///
8 | /// - Parameters:
9 | /// - checkInfo: The info object providing some general information on the lint check.
10 | /// - regex: The regex to use for matching the contents of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'.
11 | /// - violationlocation: Specifies the position of the violation marker violations should be reported. Can be the `lower` or `upper` end of a `fullMatch` or `captureGroup(index:)`.
12 | /// - matchingExamples: An array of example contents where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘.
13 | /// - nonMatchingExamples: An array of example contents where the `regex` is expected not to trigger.
14 | /// - includeFilters: An array of regexes defining which files should be incuded in the check. Will check all files matching any of the given regexes.
15 | /// - excludeFilters: An array of regexes defining which files should be excluded from the check. Will ignore all files matching any of the given regexes. Takes precedence over includes.
16 | /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection.
17 | /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly.
18 | /// - repeatIfAutoCorrected: Repeat check if at least one auto-correction was applied in last run. Defaults to `false`.
19 | public static func checkFileContents(
20 | checkInfo: CheckInfo,
21 | regex: Regex,
22 | violationLocation: ViolationLocationConfig = .init(range: .fullMatch, bound: .lower),
23 | matchingExamples: [String] = [],
24 | nonMatchingExamples: [String] = [],
25 | includeFilters: [Regex] = [#".*"#],
26 | excludeFilters: [Regex] = [],
27 | autoCorrectReplacement: String? = nil,
28 | autoCorrectExamples: [AutoCorrection] = [],
29 | repeatIfAutoCorrected: Bool = false
30 | ) throws {
31 | if !Options.unvalidated {
32 | validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo)
33 | validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo)
34 |
35 | validateParameterCombinations(
36 | checkInfo: checkInfo,
37 | autoCorrectReplacement: autoCorrectReplacement,
38 | autoCorrectExamples: autoCorrectExamples,
39 | violateIfNoMatchesFound: nil
40 | )
41 |
42 | if let autoCorrectReplacement = autoCorrectReplacement {
43 | validateAutocorrectsAll(
44 | checkInfo: checkInfo,
45 | examples: autoCorrectExamples,
46 | regex: regex,
47 | autocorrectReplacement: autoCorrectReplacement
48 | )
49 | }
50 | }
51 |
52 | guard !Options.validateOnly else {
53 | Statistics.shared.executedChecks.append(checkInfo)
54 | return
55 | }
56 |
57 | let filePathsToCheck: [String] = FilesSearch.shared.allFiles(
58 | within: fileManager.currentDirectoryPath,
59 | includeFilters: includeFilters,
60 | excludeFilters: excludeFilters
61 | )
62 |
63 | try Statistics.shared.measureTime(check: checkInfo) {
64 | let violations = try FileContentsChecker(
65 | checkInfo: checkInfo,
66 | regex: regex,
67 | violationLocation: violationLocation,
68 | filePathsToCheck: filePathsToCheck,
69 | autoCorrectReplacement: autoCorrectReplacement,
70 | repeatIfAutoCorrected: repeatIfAutoCorrected
71 | ).performCheck()
72 |
73 | Statistics.shared.found(violations: violations, in: checkInfo)
74 | }
75 | }
76 |
77 | /// Checks the names of files.
78 | ///
79 | /// - Parameters:
80 | /// - checkInfo: The info object providing some general information on the lint check.
81 | /// - regex: The regex to use for matching the paths of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'.
82 | /// - matchingExamples: An array of example paths where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘.
83 | /// - nonMatchingExamples: An array of example paths where the `regex` is expected not to trigger.
84 | /// - includeFilters: Defines which files should be incuded in check. Checks all files matching any of the given regexes.
85 | /// - excludeFilters: Defines which files should be excluded from check. Ignores all files matching any of the given regexes. Takes precedence over includes.
86 | /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection.
87 | /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly.
88 | /// - violateIfNoMatchesFound: Inverts the violation logic to report a single violation if no matches are found instead of reporting a violation for each match.
89 | public static func checkFilePaths(
90 | checkInfo: CheckInfo,
91 | regex: Regex,
92 | matchingExamples: [String] = [],
93 | nonMatchingExamples: [String] = [],
94 | includeFilters: [Regex] = [#".*"#],
95 | excludeFilters: [Regex] = [],
96 | autoCorrectReplacement: String? = nil,
97 | autoCorrectExamples: [AutoCorrection] = [],
98 | violateIfNoMatchesFound: Bool = false
99 | ) throws {
100 | if !Options.unvalidated {
101 | validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo)
102 | validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo)
103 | validateParameterCombinations(
104 | checkInfo: checkInfo,
105 | autoCorrectReplacement: autoCorrectReplacement,
106 | autoCorrectExamples: autoCorrectExamples,
107 | violateIfNoMatchesFound: violateIfNoMatchesFound
108 | )
109 |
110 | if let autoCorrectReplacement = autoCorrectReplacement {
111 | validateAutocorrectsAll(
112 | checkInfo: checkInfo,
113 | examples: autoCorrectExamples,
114 | regex: regex,
115 | autocorrectReplacement: autoCorrectReplacement
116 | )
117 | }
118 | }
119 |
120 | guard !Options.validateOnly else {
121 | Statistics.shared.executedChecks.append(checkInfo)
122 | return
123 | }
124 |
125 | let filePathsToCheck: [String] = FilesSearch.shared.allFiles(
126 | within: fileManager.currentDirectoryPath,
127 | includeFilters: includeFilters,
128 | excludeFilters: excludeFilters
129 | )
130 |
131 | try Statistics.shared.measureTime(check: checkInfo) {
132 | let violations = try FilePathsChecker(
133 | checkInfo: checkInfo,
134 | regex: regex,
135 | filePathsToCheck: filePathsToCheck,
136 | autoCorrectReplacement: autoCorrectReplacement,
137 | violateIfNoMatchesFound: violateIfNoMatchesFound
138 | ).performCheck()
139 |
140 | Statistics.shared.found(violations: violations, in: checkInfo)
141 | }
142 | }
143 |
144 | /// Run custom logic as checks.
145 | ///
146 | /// - Parameters:
147 | /// - checkInfo: The info object providing some general information on the lint check.
148 | /// - customClosure: The custom logic to run which produces an array of `Violation` objects for any violations.
149 | public static func customCheck(checkInfo: CheckInfo, customClosure: (CheckInfo) throws -> [Violation]) rethrows {
150 | try Statistics.shared.measureTime(check: checkInfo) {
151 | guard !Options.validateOnly else {
152 | Statistics.shared.executedChecks.append(checkInfo)
153 | return
154 | }
155 |
156 | Statistics.shared.found(violations: try customClosure(checkInfo), in: checkInfo)
157 | }
158 | }
159 |
160 | /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations.
161 | public static func logSummaryAndExit(arguments: [String] = [], afterPerformingChecks checksToPerform: () throws -> Void = {}) throws {
162 | let failOnWarnings = arguments.contains(Constants.strictArgument)
163 | let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue)
164 | let measure = arguments.contains(Constants.measureArgument)
165 |
166 | if targetIsXcode {
167 | log = Logger(outputType: .xcode)
168 | }
169 |
170 | log.logDebugLevel = arguments.contains(Constants.debugArgument)
171 | Options.validateOnly = arguments.contains(Constants.validateArgument)
172 |
173 | try checksToPerform()
174 |
175 | guard !Options.validateOnly else {
176 | Statistics.shared.logValidationSummary()
177 | log.exit(status: .success)
178 | return // only reachable in unit tests
179 | }
180 |
181 | Statistics.shared.logCheckSummary(printExecutionTime: measure)
182 |
183 | if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled {
184 | log.exit(status: .failure)
185 | } else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled {
186 | log.exit(status: .failure)
187 | } else {
188 | log.exit(status: .success)
189 | }
190 | }
191 |
192 | static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) {
193 | if matchingExamples.isFilled {
194 | log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug)
195 | }
196 |
197 | for example in matchingExamples where !regex.matches(example) {
198 | log.message(
199 | "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)",
200 | level: .error
201 | )
202 | log.exit(status: .failure)
203 | }
204 | }
205 |
206 | static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) {
207 | if nonMatchingExamples.isFilled {
208 | log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug)
209 | }
210 |
211 | for example in nonMatchingExamples where regex.matches(example) {
212 | log.message(
213 | "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)",
214 | level: .error
215 | )
216 | log.exit(status: .failure)
217 | }
218 | }
219 |
220 | static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) {
221 | if examples.isFilled {
222 | log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug)
223 | }
224 |
225 | for autocorrect in examples {
226 | let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement)
227 | if autocorrected != autocorrect.after {
228 | log.message(
229 | """
230 | Autocorrecting example for \(checkInfo.id) did not result in expected output.
231 | Before: '\(autocorrect.before.showWhitespacesAndNewlines())'
232 | After: '\(autocorrected.showWhitespacesAndNewlines())'
233 | Expected: '\(autocorrect.after.showWhitespacesAndNewlines())'
234 | """,
235 | level: .error
236 | )
237 | log.exit(status: .failure)
238 | }
239 | }
240 | }
241 |
242 | static func validateParameterCombinations(
243 | checkInfo: CheckInfo,
244 | autoCorrectReplacement: String?,
245 | autoCorrectExamples: [AutoCorrection],
246 | violateIfNoMatchesFound: Bool?
247 | ) {
248 | if autoCorrectExamples.isFilled && autoCorrectReplacement == nil {
249 | log.message(
250 | "`autoCorrectExamples` provided for check \(checkInfo.id) without specifying an `autoCorrectReplacement`.",
251 | level: .warning
252 | )
253 | }
254 |
255 | guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else {
256 | log.message(
257 | "Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.",
258 | level: .error
259 | )
260 | log.exit(status: .failure)
261 | return // only reachable in unit tests
262 | }
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/lint.swift:
--------------------------------------------------------------------------------
1 | #!/opt/homebrew/bin/swift-sh
2 | import AnyLint // .
3 | import Utility
4 | import ShellOut // @JohnSundell
5 |
6 | try Lint.logSummaryAndExit(arguments: CommandLine.arguments) {
7 | // MARK: - Variables
8 | let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
9 | let swiftTestFiles: Regex = #"Tests/.*\.swift"#
10 | let readmeFile: Regex = #"README\.md"#
11 | let changelogFile: Regex = #"^CHANGELOG\.md$"#
12 | let projectName: String = "AnyLint"
13 |
14 | // MARK: - Checks
15 | // MARK: Changelog
16 | try Lint.checkFilePaths(
17 | checkInfo: "Changelog: Each project should have a CHANGELOG.md file, tracking the changes within a project over time.",
18 | regex: changelogFile,
19 | matchingExamples: ["CHANGELOG.md"],
20 | nonMatchingExamples: ["CHANGELOG.markdown", "Changelog.md", "ChangeLog.md"],
21 | violateIfNoMatchesFound: true
22 | )
23 |
24 | // MARK: ChangelogEntryTrailingWhitespaces
25 | try Lint.checkFileContents(
26 | checkInfo: "ChangelogEntryTrailingWhitespaces: The summary line of a Changelog entry should end with two whitespaces.",
27 | regex: #"\n([-–] (?!None\.).*[^ ])( {0,1}| {3,})\n"#,
28 | matchingExamples: ["\n- Fixed a bug.\n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"],
29 | nonMatchingExamples: ["\n- Fixed a bug. \n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"],
30 | includeFilters: [changelogFile],
31 | autoCorrectReplacement: "\n$1 \n",
32 | autoCorrectExamples: [
33 | ["before": "\n- Fixed a bug.\n Issue:", "after": "\n- Fixed a bug. \n Issue:"],
34 | ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"],
35 | ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"],
36 | ["before": "\n- Fixed a bug !\n Issue:", "after": "\n- Fixed a bug ! \n Issue:"],
37 | ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"],
38 | ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"],
39 | ]
40 | )
41 |
42 | // MARK: ChangelogEntryLeadingWhitespaces
43 | try Lint.checkFileContents(
44 | checkInfo: "ChangelogEntryLeadingWhitespaces: The links line of a Changelog entry should start with two whitespaces.",
45 | regex: #"\n( {0,1}| {3,})(Tasks?:|Issues?:|PRs?:|Authors?:)"#,
46 | matchingExamples: ["\n- Fixed a bug.\nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)"],
47 | nonMatchingExamples: ["- Fixed a bug.\n Issue: [Link](#)"],
48 | includeFilters: [changelogFile],
49 | autoCorrectReplacement: "\n $2",
50 | autoCorrectExamples: [
51 | ["before": "\n- Fixed a bug.\nIssue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"],
52 | ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"],
53 | ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"],
54 | ]
55 | )
56 |
57 | // MARK: EmptyMethodBody
58 | try Lint.checkFileContents(
59 | checkInfo: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.",
60 | regex: ["declaration": #"(init|func [^\(\s]+)\([^{}]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#],
61 | matchingExamples: [
62 | "init() { }",
63 | "init() {\n\n}",
64 | "init(\n x: Int,\n y: Int\n) { }",
65 | "func foo2bar() { }",
66 | "func foo2bar(x: Int, y: Int) { }",
67 | "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}",
68 | ],
69 | nonMatchingExamples: ["init() { /* comment */ }", "init() {}", "func foo2bar() {}", "func foo2bar(x: Int, y: Int) {}"],
70 | includeFilters: [swiftSourceFiles, swiftTestFiles],
71 | autoCorrectReplacement: "$declaration {}",
72 | autoCorrectExamples: [
73 | ["before": "init() { }", "after": "init() {}"],
74 | ["before": "init(x: Int, y: Int) { }", "after": "init(x: Int, y: Int) {}"],
75 | ["before": "init()\n{\n \n}", "after": "init() {}"],
76 | ["before": "init(\n x: Int,\n y: Int\n) {\n \n}", "after": "init(\n x: Int,\n y: Int\n) {}"],
77 | ["before": "func foo2bar() { }", "after": "func foo2bar() {}"],
78 | ["before": "func foo2bar(x: Int, y: Int) { }", "after": "func foo2bar(x: Int, y: Int) {}"],
79 | ["before": "func foo2bar()\n{\n \n}", "after": "func foo2bar() {}"],
80 | ["before": "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", "after": "func foo2bar(\n x: Int,\n y: Int\n) {}"],
81 | ]
82 | )
83 |
84 | // MARK: EmptyTodo
85 | try Lint.checkFileContents(
86 | checkInfo: "EmptyTodo: `// TODO:` comments should not be empty.",
87 | regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#,
88 | matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"],
89 | nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"],
90 | includeFilters: [swiftSourceFiles, swiftTestFiles]
91 | )
92 |
93 | // MARK: EmptyType
94 | try Lint.checkFileContents(
95 | checkInfo: "EmptyType: Don't keep empty types in code without commenting inside why they are needed.",
96 | regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#,
97 | matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"],
98 | nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"],
99 | includeFilters: [swiftSourceFiles, swiftTestFiles]
100 | )
101 |
102 | // MARK: GuardMultiline2
103 | try Lint.checkFileContents(
104 | checkInfo: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.",
105 | regex: [
106 | "newline": #"\n"#,
107 | "guardIndent": #" *"#,
108 | "guard": #"guard *"#,
109 | "line1": #"[^\n]+,"#,
110 | "line1Indent": #"\n *"#,
111 | "line2": #"[^\n]*\S"#,
112 | "else": #"\s*else\s*\{\s*"#
113 | ],
114 | matchingExamples: [
115 | """
116 |
117 | guard let x1 = y1?.imagePath,
118 | let z = EnumType(rawValue: 15) else {
119 | return 2
120 | }
121 | """
122 | ],
123 | nonMatchingExamples: [
124 | """
125 |
126 | guard
127 | let x1 = y1?.imagePath,
128 | let z = EnumType(rawValue: 15)
129 | else {
130 | return 2
131 | }
132 | """,
133 | """
134 |
135 | guard let url = URL(string: self, relativeTo: fileManager.currentDirectoryUrl) else {
136 | return 2
137 | }
138 | """,
139 | ],
140 | includeFilters: [swiftSourceFiles, swiftTestFiles],
141 | autoCorrectReplacement: """
142 |
143 | $guardIndentguard
144 | $guardIndent $line1
145 | $guardIndent $line2
146 | $guardIndentelse {
147 | $guardIndent\u{0020}\u{0020}\u{0020}\u{0020}
148 | """,
149 | autoCorrectExamples: [
150 | [
151 | "before": """
152 | let x = 15
153 | guard let x1 = y1?.imagePath,
154 | let z = EnumType(rawValue: 15) else {
155 | return 2
156 | }
157 | """,
158 | "after": """
159 | let x = 15
160 | guard
161 | let x1 = y1?.imagePath,
162 | let z = EnumType(rawValue: 15)
163 | else {
164 | return 2
165 | }
166 | """
167 | ],
168 | ]
169 | )
170 |
171 | // MARK: GuardMultiline3
172 | try Lint.checkFileContents(
173 | checkInfo: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.",
174 | regex: [
175 | "newline": #"\n"#,
176 | "guardIndent": #" *"#,
177 | "guard": #"guard *"#,
178 | "line1": #"[^\n]+,"#,
179 | "line1Indent": #"\n *"#,
180 | "line2": #"[^\n]+,"#,
181 | "line2Indent": #"\n *"#,
182 | "line3": #"[^\n]*\S"#,
183 | "else": #"\s*else\s*\{\s*"#
184 | ],
185 | matchingExamples: [
186 | """
187 |
188 | guard let x1 = y1?.imagePath,
189 | let x2 = y2?.imagePath,
190 | let z = EnumType(rawValue: 15) else {
191 | return 2
192 | }
193 | """
194 | ],
195 | nonMatchingExamples: [
196 | """
197 |
198 | guard
199 | let x1 = y1?.imagePath,
200 | let x2 = y2?.imagePath,
201 | let z = EnumType(rawValue: 15)
202 | else {
203 | return 2
204 | }
205 | """,
206 | """
207 |
208 | guard let url = URL(x: 1, y: 2, relativeTo: fileManager.currentDirectoryUrl) else {
209 | return 2
210 | }
211 | """,
212 | ],
213 | includeFilters: [swiftSourceFiles, swiftTestFiles],
214 | autoCorrectReplacement: """
215 |
216 | $guardIndentguard
217 | $guardIndent $line1
218 | $guardIndent $line2
219 | $guardIndent $line3
220 | $guardIndentelse {
221 | $guardIndent\u{0020}\u{0020}\u{0020}\u{0020}
222 | """,
223 | autoCorrectExamples: [
224 | [
225 | "before": """
226 | let x = 15
227 | guard let x1 = y1?.imagePath,
228 | let x2 = y2?.imagePath,
229 | let z = EnumType(rawValue: 15) else {
230 | return 2
231 | }
232 | """,
233 | "after": """
234 | let x = 15
235 | guard
236 | let x1 = y1?.imagePath,
237 | let x2 = y2?.imagePath,
238 | let z = EnumType(rawValue: 15)
239 | else {
240 | return 2
241 | }
242 | """
243 | ],
244 | ]
245 | )
246 |
247 | // MARK: GuardMultiline4
248 | try Lint.checkFileContents(
249 | checkInfo: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.",
250 | regex: [
251 | "newline": #"\n"#,
252 | "guardIndent": #" *"#,
253 | "guard": #"guard *"#,
254 | "line1": #"[^\n]+,"#,
255 | "line1Indent": #"\n *"#,
256 | "line2": #"[^\n]+,"#,
257 | "line2Indent": #"\n *"#,
258 | "line3": #"[^\n]+,"#,
259 | "line3Indent": #"\n *"#,
260 | "line4": #"[^\n]*\S"#,
261 | "else": #"\s*else\s*\{\s*"#
262 | ],
263 | matchingExamples: [
264 | """
265 |
266 | guard let x1 = y1?.imagePath,
267 | let x2 = y2?.imagePath,
268 | let x3 = y3?.imagePath,
269 | let z = EnumType(rawValue: 15) else {
270 | return 2
271 | }
272 | """
273 | ],
274 | nonMatchingExamples: [
275 | """
276 |
277 | guard
278 | let x1 = y1?.imagePath,
279 | let x2 = y2?.imagePath,
280 | let x3 = y3?.imagePath,
281 | let z = EnumType(rawValue: 15)
282 | else {
283 | return 2
284 | }
285 | """,
286 | """
287 |
288 | guard let url = URL(x: 1, y: 2, z: 3, relativeTo: fileManager.currentDirectoryUrl) else {
289 | return 2
290 | }
291 | """,
292 | ],
293 | includeFilters: [swiftSourceFiles, swiftTestFiles],
294 | autoCorrectReplacement: """
295 |
296 | $guardIndentguard
297 | $guardIndent $line1
298 | $guardIndent $line2
299 | $guardIndent $line3
300 | $guardIndent $line4
301 | $guardIndentelse {
302 | $guardIndent\u{0020}\u{0020}\u{0020}\u{0020}
303 | """,
304 | autoCorrectExamples: [
305 | [
306 | "before": """
307 | let x = 15
308 | guard let x1 = y1?.imagePath,
309 | let x2 = y2?.imagePath,
310 | let x3 = y3?.imagePath,
311 | let z = EnumType(rawValue: 15) else {
312 | return 2
313 | }
314 | """,
315 | "after": """
316 | let x = 15
317 | guard
318 | let x1 = y1?.imagePath,
319 | let x2 = y2?.imagePath,
320 | let x3 = y3?.imagePath,
321 | let z = EnumType(rawValue: 15)
322 | else {
323 | return 2
324 | }
325 | """
326 | ],
327 | ]
328 | )
329 |
330 | // MARK: GuardMultilineN
331 | try Lint.checkFileContents(
332 | checkInfo: "GuardMultilineN: Close a multiline guard via `else {` on a new line indented like the opening `guard`.",
333 | regex: #"\n *guard *([^\n]+,\n){4,}[^\n]*\S\s*else\s*\{\s*"#,
334 | matchingExamples: [
335 | """
336 |
337 | guard let x1 = y1?.imagePath,
338 | let x2 = y2?.imagePath,
339 | let x3 = y3?.imagePath,
340 | let x4 = y4?.imagePath,
341 | let x5 = y5?.imagePath,
342 | let z = EnumType(rawValue: 15) else {
343 | return 2
344 | }
345 | """
346 | ],
347 | nonMatchingExamples: [
348 | """
349 |
350 | guard
351 | let x1 = y1?.imagePath,
352 | let x2 = y2?.imagePath,
353 | let x3 = y3?.imagePath,
354 | let x4 = y4?.imagePath,
355 | let x5 = y5?.imagePath,
356 | let z = EnumType(rawValue: 15)
357 | else {
358 | return 2
359 | }
360 | """,
361 | """
362 |
363 | guard let url = URL(x1: 1, x2: 2, x3: 3, x4: 4, x5: 5, relativeTo: fileManager.currentDirectoryUrl) else {
364 | return 2
365 | }
366 | """,
367 | ],
368 | includeFilters: [swiftSourceFiles, swiftTestFiles]
369 | )
370 |
371 | // MARK: IfAsGuard
372 | try Lint.checkFileContents(
373 | checkInfo: "IfAsGuard: Don't use an if statement to just return – use guard for such cases instead.",
374 | regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#,
375 | matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"],
376 | nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"],
377 | includeFilters: [swiftSourceFiles, swiftTestFiles]
378 | )
379 |
380 | // MARK: LateForceUnwrapping3
381 | try Lint.checkFileContents(
382 | checkInfo: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.",
383 | regex: [
384 | "openingBrace": #"\("#,
385 | "callPart1": #"[^\s\?\.]+"#,
386 | "separator1": #"\?\."#,
387 | "callPart2": #"[^\s\?\.]+"#,
388 | "separator2": #"\?\."#,
389 | "callPart3": #"[^\s\?\.]+"#,
390 | "separator3": #"\?\."#,
391 | "callPart4": #"[^\s\?\.]+"#,
392 | "closingBraceUnwrap": #"\)!"#,
393 | ],
394 | matchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n"],
395 | nonMatchingExamples: ["call(x: (viewModel?.username)!)", "let x = viewModel!.user!.profile!.imagePath\n"],
396 | includeFilters: [swiftSourceFiles, swiftTestFiles],
397 | autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3!.$callPart4",
398 | autoCorrectExamples: [
399 | ["before": "let x = (viewModel?.user?.profile?.imagePath)!\n", "after": "let x = viewModel!.user!.profile!.imagePath\n"],
400 | ]
401 | )
402 |
403 | // MARK: LateForceUnwrapping2
404 | try Lint.checkFileContents(
405 | checkInfo: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.",
406 | regex: [
407 | "openingBrace": #"\("#,
408 | "callPart1": #"[^\s\?\.]+"#,
409 | "separator1": #"\?\."#,
410 | "callPart2": #"[^\s\?\.]+"#,
411 | "separator2": #"\?\."#,
412 | "callPart3": #"[^\s\?\.]+"#,
413 | "closingBraceUnwrap": #"\)!"#,
414 | ],
415 | matchingExamples: ["call(x: (viewModel?.profile?.username)!)"],
416 | nonMatchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n", "let x = viewModel!.profile!.imagePath\n"],
417 | includeFilters: [swiftSourceFiles, swiftTestFiles],
418 | autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3",
419 | autoCorrectExamples: [
420 | ["before": "let x = (viewModel?.profile?.imagePath)!\n", "after": "let x = viewModel!.profile!.imagePath\n"],
421 | ]
422 | )
423 |
424 | // MARK: LateForceUnwrapping1
425 | try Lint.checkFileContents(
426 | checkInfo: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.",
427 | regex: [
428 | "openingBrace": #"\("#,
429 | "callPart1": #"[^\s\?\.]+"#,
430 | "separator1": #"\?\."#,
431 | "callPart2": #"[^\s\?\.]+"#,
432 | "closingBraceUnwrap": #"\)!"#,
433 | ],
434 | matchingExamples: ["call(x: (viewModel?.username)!)"],
435 | nonMatchingExamples: ["call(x: (viewModel?.profile?.username)!)", "call(x: viewModel!.username)"],
436 | includeFilters: [swiftSourceFiles, swiftTestFiles],
437 | autoCorrectReplacement: "$callPart1!.$callPart2",
438 | autoCorrectExamples: [
439 | ["before": "call(x: (viewModel?.username)!)", "after": "call(x: viewModel!.username)"],
440 | ]
441 | )
442 |
443 | // MARK: LinuxMainUpToDate
444 | try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in
445 | var violations: [Violation] = []
446 |
447 | let linuxMainFilePath = "Tests/LinuxMain.swift"
448 | let linuxMainContentsBeforeRegeneration = try! String(contentsOfFile: linuxMainFilePath)
449 |
450 | let sourceryDirPath = ".sourcery"
451 | let testsDirPath = "Tests/\(projectName)Tests"
452 | let stencilFilePath = "\(sourceryDirPath)/LinuxMain.stencil"
453 | let generatedLinuxMainFilePath = "\(sourceryDirPath)/LinuxMain.generated.swift"
454 |
455 | let sourceryInstallPath = try? shellOut(to: "which", arguments: ["sourcery"])
456 | guard sourceryInstallPath != nil else {
457 | log.message(
458 | "Skipped custom check \(checkInfo) – requires Sourcery to be installed, download from: https://github.com/krzysztofzablocki/Sourcery",
459 | level: .warning
460 | )
461 | return []
462 | }
463 |
464 | try! shellOut(to: "sourcery", arguments: ["--sources", testsDirPath, "--templates", stencilFilePath, "--output", sourceryDirPath])
465 | let linuxMainContentsAfterRegeneration = try! String(contentsOfFile: generatedLinuxMainFilePath)
466 |
467 | // move generated file to LinuxMain path to update its contents
468 | try! shellOut(to: "mv", arguments: [generatedLinuxMainFilePath, linuxMainFilePath])
469 |
470 | if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration {
471 | violations.append(
472 | Violation(
473 | checkInfo: checkInfo,
474 | filePath: linuxMainFilePath,
475 | appliedAutoCorrection: AutoCorrection(
476 | before: linuxMainContentsBeforeRegeneration,
477 | after: linuxMainContentsAfterRegeneration
478 | )
479 | )
480 | )
481 | }
482 |
483 | return violations
484 | }
485 |
486 | // MARK: Logger
487 | try Lint.checkFileContents(
488 | checkInfo: "Logger: Don't use `print` – use `log.message` instead.",
489 | regex: #"print\([^\n]+\)"#,
490 | matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#],
491 | nonMatchingExamples: [#"log.message("Hello world!")"#],
492 | includeFilters: [swiftSourceFiles, swiftTestFiles],
493 | excludeFilters: [#"Sources/.*/Logger\.swift"#]
494 | )
495 |
496 | // MARK: Readme
497 | try Lint.checkFilePaths(
498 | checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.",
499 | regex: #"^README\.md$"#,
500 | matchingExamples: ["README.md"],
501 | nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"],
502 | violateIfNoMatchesFound: true
503 | )
504 |
505 | // MARK: ReadmePath
506 | try Lint.checkFilePaths(
507 | checkInfo: "ReadmePath: The README file should be named exactly `README.md`.",
508 | regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#,
509 | matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"],
510 | nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"],
511 | autoCorrectReplacement: "$1README.md",
512 | autoCorrectExamples: [
513 | ["before": "api/readme.md", "after": "api/README.md"],
514 | ["before": "ReadMe.md", "after": "README.md"],
515 | ["before": "README.markdown", "after": "README.md"],
516 | ]
517 | )
518 | }
519 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
14 |
15 |
16 |
18 |
19 |
20 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 | Installation
44 | • Getting Started
45 | • Configuration
46 | • Xcode Build Script
47 | • Donation
48 | • Issues
49 | • Regex Cheat Sheet
50 | • License
51 |
52 |
53 | # AnyLint
54 |
55 | 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! 🎉
56 |
57 | ## Installation
58 |
59 | ### Via [Homebrew](https://brew.sh):
60 |
61 | To **install** AnyLint the first time, run these commands:
62 |
63 | ```bash
64 | brew tap FlineDev/AnyLint https://github.com/FlineDev/AnyLint.git
65 | brew install anylint
66 | ```
67 |
68 | To **update** it to the latest version, run this instead:
69 |
70 | ```bash
71 | brew upgrade anylint
72 | ```
73 |
74 | ### Via [Mint](https://github.com/yonaskolb/Mint):
75 |
76 | To **install** AnyLint or **update** to the latest version, run this command:
77 |
78 | ```bash
79 | mint install FlineDev/AnyLint
80 | ```
81 |
82 | ## Getting Started
83 |
84 | To initialize AnyLint in a project, run:
85 |
86 | ```bash
87 | anylint --init blank
88 | ```
89 |
90 | This will create the Swift script file `lint.swift` with something like the following contents:
91 |
92 | ```swift
93 | #!/opt/local/bin/swift-sh
94 | import AnyLint // @FlineDev
95 |
96 | Lint.logSummaryAndExit(arguments: CommandLine.arguments) {
97 | // MARK: - Variables
98 | let readmeFile: Regex = #"README\.md"#
99 |
100 | // MARK: - Checks
101 | // MARK: Readme
102 | try Lint.checkFilePaths(
103 | checkInfo: "Readme: Each project should have a README.md file explaining the project.",
104 | regex: readmeFile,
105 | matchingExamples: ["README.md"],
106 | nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"],
107 | violateIfNoMatchesFound: true
108 | )
109 |
110 | // MARK: ReadmeTypoLicense
111 | try Lint.checkFileContents(
112 | checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.",
113 | regex: #"([\s#]L|l)isence([\s\.,:;])"#,
114 | matchingExamples: [" license:", "## Lisence\n"],
115 | nonMatchingExamples: [" license:", "## License\n"],
116 | includeFilters: [readmeFile],
117 | autoCorrectReplacement: "$1icense$2",
118 | autoCorrectExamples: [
119 | ["before": " lisence:", "after": " license:"],
120 | ["before": "## Lisence\n", "after": "## License\n"],
121 | ]
122 | )
123 | }
124 |
125 | ```
126 |
127 | The most important thing to note is that the **first three lines are required** for AnyLint to work properly.
128 |
129 | All the other code 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](https://github.com/mxcl/swift-sh). Thus, you can run any Swift code and even import Swift packages (see the [swift-sh docs](https://github.com/mxcl/swift-sh#usage)) if you need to. The third line makes sure that all violations found in the process of running the code in the completion block are reported properly and exits the script with the proper exit code at the end.
130 |
131 | 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](#configuration) section below.
132 |
133 | 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:
134 |
135 | Initializes the configuration files at the given locations:
136 | ```bash
137 | anylint --init blank --path Sources/lint.swift --path Tests/lint.swift
138 | ```
139 |
140 | Runs the lint checks for both configuration files:
141 | ```bash
142 | anylint --path Sources/lint.swift --path Tests/lint.swift
143 | ```
144 |
145 | There are also several flags you can pass to `anylint`:
146 |
147 | 1. `-s` / `--strict`: Fails on warnings as well. (By default, the command only fails on errors.)
148 | 1. `-x` / `--xcode`: Prints warnings & errors in a format to be reported right within Xcodes left sidebar.
149 | 1. `-l` / `--validate`: Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`.
150 | 1. `-u` / `--unvalidated`: Runs the checks without validating their correctness. Only use for faster subsequent runs after a validated run succeeded.
151 | 1. `-m` / `--measure`: Prints the time it took to execute each check for performance optimizations.
152 | 1. `-v` / `--version`: Prints the current tool version. (Does not run any lint checks.)
153 | 1. `-d` / `--debug`: Logs much more detailed information about what AnyLint is doing for debugging purposes.
154 |
155 | ## Configuration
156 |
157 | AnyLint provides three different kinds of lint checks:
158 |
159 | 1. `checkFileContents`: Matches the contents of a text file to a given regex.
160 | 2. `checkFilePaths`: Matches the file paths of the current directory to a given regex.
161 | 3. `customCheck`: Allows to write custom Swift code to do other kinds of checks.
162 |
163 | Several examples of lint checks can be found in the [`lint.swift` file of this very project](https://github.com/FlineDev/AnyLint/blob/main/lint.swift).
164 |
165 | ### Basic Types
166 |
167 | Independent from the method used, there are a few types specified in the AnyLint package you should know of.
168 |
169 | #### Regex
170 |
171 | Many parameters in the above mentioned lint check methods are of `Regex` type. A `Regex` can be initialized in several ways:
172 |
173 | 1. Using a **String**:
174 | ```swift
175 | let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/
176 | let regexWithOptions = Regex(#"(foo|bar)[0-9]+"#, options: [.ignoreCase, .dotMatchesLineSeparators, .anchorsMatchLines]) // => /(foo|bar)[0-9]+/im
177 | ```
178 | 2. Using a **String Literal**:
179 | ```swift
180 | let regex: Regex = #"(foo|bar)[0-9]+"# // => /(foo|bar)[0-9]+/
181 | let regexWithOptions: Regex = #"(foo|bar)[0-9]+\im"# // => /(foo|bar)[0-9]+/im
182 | ```
183 | 3. Using a **Dictionary Literal**: (use for [named capture groups](https://www.regular-expressions.info/named.html))
184 | ```swift
185 | let regex: Regex = ["key": #"foo|bar"#, "num": "[0-9]+"] // => /(?foo|bar)(?[0-9]+)/
186 | let regexWithOptions: Regex = ["key": #"foo|bar"#, "num": "[0-9]+", #"\"#: "im"] // => /(?foo|bar)(?[0-9]+)/im
187 | ```
188 |
189 | Note that we recommend using [raw strings](https://www.hackingwithswift.com/articles/162/how-to-use-raw-strings-in-swift) (`#"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](https://rubular.com/) first and then copy & pasting from them without any additional escaping (except for `{` & `}`, replace with `\{` & `\}`).
190 |
191 |
192 | Regex Options
193 |
194 | Specifying Regex options in literals is done via the `\` separator as shown in the examples above. The available options are:
195 |
196 | 1. `i` for `.ignoreCase`: Any specified characters will both match uppercase and lowercase variants.
197 | 2. `m` for `.dotMatchesLineSeparators`: All appearances of `.` in regexes will also match newlines (which are not matched against by default).
198 |
199 | The `.anchorsMatchLines` option is always activated on literal usage as we strongly recommend it. It ensures that `^` can be used to match the start of a line and `$` for the end of a line. By default they would match the start & end of the _entire string_. If that's actually what you want, you can still use `\A` and `\z` for that. This makes the default literal Regex behavior more in line with sites like [Rubular](https://rubular.com/).
200 |
201 |
202 |
203 | #### CheckInfo
204 |
205 | A `CheckInfo` contains the basic information about a lint check. It consists of:
206 |
207 | 1. `id`: The identifier of your lint check. For example: `EmptyTodo`
208 | 2. `hint`: The hint explaining the cause of the violation or the steps to fix it.
209 | 3. `severity`: The severity of violations. One of `error`, `warning`, `info`. Default: `error`
210 |
211 | While there is an initializer available, we recommend using a String Literal instead like so:
212 |
213 | ```swift
214 | // accepted structure: (@):
215 | let checkInfo: CheckInfo = "ReadmePath: The README file should be named exactly `README.md`."
216 | let checkInfoCustomSeverity: CheckInfo = "ReadmePath@warning: The README file should be named exactly `README.md`."
217 | ```
218 |
219 | #### AutoCorrection
220 |
221 | An `AutoCorrection` contains an example `before` and `after` string to validate that a given autocorrection rule behaves correctly.
222 |
223 | It can be initialized in two ways, either with the default initializer:
224 |
225 | ```swift
226 | let example: AutoCorrection = AutoCorrection(before: "Lisence", after: "License")
227 | ```
228 |
229 | Or using a Dictionary literal:
230 |
231 | ```swift
232 | let example: AutoCorrection = ["before": "Lisence", "after": "License"]
233 | ```
234 |
235 | ### Check File Contents
236 |
237 | 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.
238 |
239 | In its simplest form, the method just requires a `checkInfo` and a `regex`:
240 |
241 | ```swift
242 | // MARK: EmptyTodo
243 | try Lint.checkFileContents(
244 | checkInfo: "EmptyTodo: TODO comments should not be empty.",
245 | regex: #"// TODO: *\n"#
246 | )
247 | ```
248 |
249 | But we *strongly recommend* to always provide also:
250 |
251 | 1. `matchingExamples`: Array of strings expected to match the given string for `regex` validation.
252 | 2. `nonMatchingExamples`: Array of strings not matching the given string for `regex` validation.
253 | 3. `includeFilters`: Array of `Regex` objects to include to the file paths to check.
254 |
255 | 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.
256 |
257 | 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.
258 |
259 | Here's the *recommended minimum example*:
260 |
261 | ```swift
262 | // MARK: - Variables
263 | let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
264 | let swiftTestFiles: Regex = #"Tests/.*\.swift"#
265 |
266 | // MARK: - Checks
267 | // MARK: empty_todo
268 | try Lint.checkFileContents(
269 | checkInfo: "EmptyTodo: TODO comments should not be empty.",
270 | regex: #"// TODO: *\n"#,
271 | matchingExamples: ["// TODO:\n"],
272 | nonMatchingExamples: ["// TODO: not yet implemented\n"],
273 | includeFilters: [swiftSourceFiles, swiftTestFiles]
274 | )
275 | ```
276 |
277 | There's 5 more parameters you can optionally set if needed:
278 |
279 | 1. `excludeFilters`: Array of `Regex` objects to exclude from the file paths to check.
280 | 1. `violationLocation`: Specifies the position of the violation marker violations should be reported. Can be the `lower` or `upper` end of a `fullMatch` or `captureGroup(index:)`.
281 | 1. `autoCorrectReplacement`: Replacement string which can reference any capture groups in the `regex`.
282 | 1. `autoCorrectExamples`: Example structs with `before` and `after` for autocorrection validation.
283 | 1. `repeatIfAutoCorrected`: Repeat check if at least one auto-correction was applied in last run. Defaults to `false`.
284 |
285 | The `excludeFilters` can be used alternatively to the `includeFilters` or alongside them. If used alongside, exclusion will take precedence over inclusion.
286 |
287 | 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 (`(?[a-z])(?\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.
288 |
289 | > *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.
290 |
291 | Here's a *full example using all parameters* at once:
292 |
293 | ```swift
294 | // MARK: - Variables
295 | let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
296 | let swiftTestFiles: Regex = #"Tests/.*\.swift"#
297 |
298 | // MARK: - Checks
299 | // MARK: empty_method_body
300 | try Lint.checkFileContents(
301 | checkInfo: "EmptyMethodBody: Don't use whitespaces for the body of empty methods.",
302 | regex: [
303 | "declaration": #"func [^\(\s]+\([^{]*\)"#,
304 | "spacing": #"\s*"#,
305 | "body": #"\{\s+\}"#
306 | ],
307 | violationLocation: .init(range: .fullMatch, bound: .upper),
308 | matchingExamples: [
309 | "func foo2bar() { }",
310 | "func foo2bar(x: Int, y: Int) { }",
311 | "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}",
312 | ],
313 | nonMatchingExamples: [
314 | "func foo2bar() {}",
315 | "func foo2bar(x: Int, y: Int) {}"
316 | ],
317 | includeFilters: [swiftSourceFiles],
318 | excludeFilters: [swiftTestFiles],
319 | autoCorrectReplacement: "$declaration {}",
320 | autoCorrectExamples: [
321 | ["before": "func foo2bar() { }", "after": "func foo2bar() {}"],
322 | ["before": "func foo2bar(x: Int, y: Int) { }", "after": "func foo2bar(x: Int, y: Int) {}"],
323 | ["before": "func foo2bar()\n{\n \n}", "after": "func foo2bar() {}"],
324 | ]
325 | )
326 | ```
327 |
328 | Note that when `autoCorrectReplacement` produces a replacement string that exactly matches the matched string of `regex`, then no violation will be reported. This enables us to provide more generic `regex` patterns that also match the correct string without actually reporting a violation for the correct one. For example, using the regex ` if\s*\(([^)]+)\)\s*\{` to check whitespaces around braces after `if` statement would report a violation for all of the following examples:
329 |
330 | ```Java
331 | if(x == 5) { /* some code */ }
332 | if (x == 5){ /* some code */ }
333 | if(x == 5){ /* some code */ }
334 | if (x == 5) { /* some code */ }
335 | ```
336 |
337 | The problem is that the last example actually is our expected formatting and should not violate. By providing an `autoCorrectReplacement` of ` if ($1) {`, we can fix that as the replacement would be equal to the matched string, so no violation would be reported for the last example and all the others would be auto-corrected – just what we want. 🎉
338 |
339 | (The alternative would be to split the check to two separate ones, one fore checking the prefix and one the suffix whitespacing – not so beautiful as this blows up our `lint.swift` configuration file very quickly.)
340 |
341 | #### Skip file content checks
342 |
343 | While the `includeFilters` and `excludeFilters` arguments in the config file can be used to skip checks on specified files, sometimes it's necessary to make **exceptions** and specify that within the files themselves. For example this can become handy when there's a check which works 99% of the time, but there might be the 1% of cases where the check is reporting **false positives**.
344 |
345 | For such cases, there are **2 ways to skip checks** within the files themselves:
346 |
347 | 1. `AnyLint.skipHere: `: Will skip the specified check(s) on the same line and the next line.
348 |
349 | ```swift
350 | var x: Int = 5 // AnyLint.skipHere: MinVarNameLength
351 |
352 | // or
353 |
354 | // AnyLint.skipHere: MinVarNameLength
355 | var x: Int = 5
356 | ```
357 |
358 | 2. `AnyLint.skipInFile: `: Will skip `All` or specificed check(s) in the entire file.
359 |
360 | ```swift
361 | // AnyLint.skipInFile: MinVarNameLength
362 |
363 | var x: Int = 5
364 | var y: Int = 5
365 | ```
366 | or
367 |
368 | ```swift
369 | // AnyLint.skipInFile: All
370 |
371 | var x: Int = 5
372 | var y: Int = 5
373 | ```
374 |
375 | It is also possible to skip multiple checks at once in a line like so:
376 |
377 | ```swift
378 | // AnyLint.skipHere: MinVarNameLength, LineLength, ColonWhitespaces
379 | ```
380 |
381 | ### Check File Paths
382 |
383 | 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:
384 |
385 | 1. `autoCorrectReplacement`: Here, this will safely move the file using the path replacement.
386 | 2. `violateIfNoMatchesFound`: Will report a violation if _no_ matches are found if `true`. Default: `false`
387 |
388 | 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.
389 |
390 | 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_ match any file.
391 |
392 | ### Custom Checks
393 |
394 | 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](https://swiftpm.co/). 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:
395 |
396 | 1. `checkInfo`: Provides some general information on the lint check.
397 | 2. `customClosure`: Your custom logic which produces an array of `Violation` objects.
398 |
399 | 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.
400 |
401 | 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](https://github.com/FlineDev/HandySwift#regex) (the project, the class was taken from) or read the [code documentation comments](https://github.com/FlineDev/AnyLint/blob/main/Sources/Utility/Regex.swift).
402 |
403 | When using the `customCheck`, you might want to also include some Swift packages for [easier file handling](https://github.com/JohnSundell/Files) or [running shell commands](https://github.com/JohnSundell/ShellOut). You can do so by adding them at the top of the file like so:
404 |
405 | ```swift
406 | #!/opt/local/bin/swift-sh
407 | import AnyLint // @FlineDev
408 | import ShellOut // @JohnSundell
409 |
410 | Lint.logSummaryAndExit(arguments: CommandLine.arguments) {
411 | // MARK: - Variables
412 | let projectName: String = "AnyLint"
413 |
414 | // MARK: - Checks
415 | // MARK: LinuxMainUpToDate
416 | try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in
417 | var violations: [Violation] = []
418 |
419 | let linuxMainFilePath = "Tests/LinuxMain.swift"
420 | let linuxMainContentsBeforeRegeneration = try! String(contentsOfFile: linuxMainFilePath)
421 |
422 | let sourceryDirPath = ".sourcery"
423 | try! shellOut(to: "sourcery", arguments: ["--sources", "Tests/\(projectName)Tests", "--templates", "\(sourceryDirPath)/LinuxMain.stencil", "--output", sourceryDirPath])
424 |
425 | let generatedLinuxMainFilePath = "\(sourceryDirPath)/LinuxMain.generated.swift"
426 | let linuxMainContentsAfterRegeneration = try! String(contentsOfFile: generatedLinuxMainFilePath)
427 |
428 | // move generated file to LinuxMain path to update its contents
429 | try! shellOut(to: "mv", arguments: [generatedLinuxMainFilePath, linuxMainFilePath])
430 |
431 | if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration {
432 | violations.append(
433 | Violation(
434 | checkInfo: checkInfo,
435 | filePath: linuxMainFilePath,
436 | appliedAutoCorrection: AutoCorrection(
437 | before: linuxMainContentsBeforeRegeneration,
438 | after: linuxMainContentsAfterRegeneration
439 | )
440 | )
441 | )
442 | }
443 |
444 | return violations
445 | }
446 | }
447 |
448 | ```
449 |
450 | ## Xcode Build Script
451 |
452 | 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:
453 |
454 | ```bash
455 | export PATH="$PATH:/opt/homebrew/bin"
456 |
457 | if which anylint > /dev/null; then
458 | anylint -x
459 | else
460 | echo "warning: AnyLint not installed, see from https://github.com/FlineDev/AnyLint"
461 | fi
462 | ```
463 |
464 | 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`.
465 |
466 | > **_Note_**: There's a [known bug](https://github.com/mxcl/swift-sh/issues/113) when the build script is used in non-macOS platforms targets.
467 |
468 | ## Regex Cheat Sheet
469 |
470 | Refer to the Regex quick reference on [rubular.com](https://rubular.com/) which all apply for Swift as well:
471 |
472 |
474 |
475 |
476 | In Swift, there are some **differences to regexes in Ruby** (which rubular.com is based on) – take care when copying regexes:
477 |
478 | 1. In Ruby, forward slashes (`/`) must be escaped (`\/`), that's not necessary in Swift.
479 | 2. In Swift, curly braces (`{` & `}`) must be escaped (`\{` & `\}`), that's not necessary in Ruby.
480 |
481 | Here are some **advanced Regex features** you might want to use or learn more about:
482 |
483 | 1. Back references can be used within regexes to match previous capture groups.
484 |
485 | For example, you can make sure that the PR number and link match in `PR: [#100](https://github.com/FlineDev/AnyLint/pull/100)` by using a capture group (`(\d+)`) and a back reference (`\1`) like in: `\[#(\d+)\]\(https://[^)]+/pull/\1\)`.
486 |
487 | [Learn more](https://www.regular-expressions.info/backref.html)
488 |
489 | 2. Negative & positive lookaheads & lookbehinds allow you to specify patterns with some limitations that will be excluded from the matched range. They are specified with `(?=PATTERN)` (positive lookahead), `(?!PATTERN)` (negative lookahead), `(?<=PATTERN)` (positive lookbehind) or `(?