Skip to content
This repository has been archived by the owner on Mar 10, 2022. It is now read-only.

Added the allowed_paths_regex subrule #34

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
### Added
- Make the rule `File Content Regex` print the offending lines.
Issue: [#31](https://github.com/JamitLabs/ProjLint/issues/31) | PR: [#32](https://github.com/JamitLabs/ProjLint/pull/32) | Author: [Andrés Cecilia Luque](https://github.com/acecilia)
- Added the `allowed_paths_regex` subrule under the file existance rule. Now it is possible to specify the allowed paths in a project by using multiple regexes.
Issues: [#16](https://github.com/JamitLabs/ProjLint/issues/16), [#20](https://github.com/JamitLabs/ProjLint/issues/20) | PR: [#34](https://github.com/JamitLabs/ProjLint/pull/34) | Author: [Andrés Cecilia Luque](https://github.com/acecilia)
### Changed
- Replaced `lint_fail_level` configuration option with `strict` command line argument. Specify `--strict` or `-s` if you want the tool to fail on warnings.
### Deprecated
Expand Down
7 changes: 7 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Option | Type | Required? | Description
--- | --- | --- | ---
`existing_paths` | `[String]` | no | Files that must exist.
`non_existing_paths` | `[String]` | no | Files that must not exist.
`allowed_paths_regex` | `[String]` | no | A list of regexes matching only allowed paths: files with a path that do not match the regex will trigger a violation.

<details>
<summary>Example</summary>
Expand All @@ -120,6 +121,12 @@ rules:
non_existing_paths:
- Podfile
- Podfile.lock
allowed_paths_regex:
- (Sources|Tests)/.+\.swift # Sources
- (Resources|Formula)/.+ # Other necessary resources
- \.(build|sourcery|git|templates)/.+ # Necessary files under hidden directories
- ProjLint\.xcodeproj/.+ # Xcode project
- '[^/]+' # Root files (needs quotation because the regex contains reserved yaml characters)
```

</details>
Expand Down
6 changes: 5 additions & 1 deletion Sources/ProjLintKit/Rules/FileExistenceOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import Foundation
class FileExistenceOptions: RuleOptions {
let existingPaths: [String]?
let nonExistingPaths: [String]?
let allowedPathsRegex: [String]?

override init(_ optionsDict: [String: Any], rule: Rule.Type) {
let existingPaths = RuleOptions.optionalStringArray(forOption: "existing_paths", in: optionsDict, rule: rule)
let nonExistingPaths = RuleOptions.optionalStringArray(forOption: "non_existing_paths", in: optionsDict, rule: rule)
let allowedPathsRegex = RuleOptions.optionalStringArray(forOption: "allowed_paths_regex", in: optionsDict, rule: rule)

guard existingPaths != nil || nonExistingPaths != nil else {
acecilia marked this conversation as resolved.
Show resolved Hide resolved
let options = [existingPaths, nonExistingPaths, allowedPathsRegex]
guard options.contains(where: { $0 != nil }) else {
print("Rule \(rule.identifier) must have at least one option specified.", level: .error)
exit(EX_USAGE)
}

self.existingPaths = existingPaths
self.nonExistingPaths = nonExistingPaths
self.allowedPathsRegex = allowedPathsRegex

super.init(optionsDict, rule: rule)
}
Expand Down
71 changes: 71 additions & 0 deletions Sources/ProjLintKit/Rules/FileExistenceRule.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import HandySwift

struct FileExistenceRule: Rule {
static let name: String = "File Existence"
Expand Down Expand Up @@ -46,6 +47,76 @@ struct FileExistenceRule: Rule {
}
}

if let allowedPathsRegex = options.allowedPathsRegex {
let allowedPathsViolations = violationsForAllowedPaths(allowedPathsRegex, in: directory)
violations.append(contentsOf: allowedPathsViolations)
}

return violations
}

private func violationsForAllowedPaths(_ allowedPathsRegex: [String], in directory: URL) -> [Violation] {
var violations: [Violation] = []

// Start by getting an array of all files under the directory.
// After, remove all files that are allowed, until ending up with the list of notAllowedFiles
var notAllowedFiles = recursivelyGetFiles(at: directory)

// Do not check for paths that are already linted by previous projlint rules
let existingPaths = options.existingPaths?.map { URL(fileURLWithPath: $0, relativeTo: directory) } ?? []
let nonExistingPaths = options.nonExistingPaths?.map { URL(fileURLWithPath: $0, relativeTo: directory) } ?? []
let pathsAlreadyLinted = existingPaths + nonExistingPaths
notAllowedFiles.removeAll { existingFile in
pathsAlreadyLinted.contains { $0.path == existingFile.path }
}

for allowedPathPattern in allowedPathsRegex {
guard let allowedPathRegex = try? Regex("^\(allowedPathPattern)$") else {
let violation = Violation(
rule: self,
message: "The following regex is not valid: '\(allowedPathPattern)'",
level: .error
)
violations.append(violation)
break
}

notAllowedFiles.removeAll { allowedPathRegex.matches($0.relativePath) }
}

notAllowedFiles.forEach {
let violation = FileViolation(
rule: self,
message: "File exists, but it mustn't.",
level: options.violationLevel(defaultTo: defaultViolationLevel),
url: $0
)
violations.append(violation)
}

return violations
}

private func recursivelyGetFiles(at currentUrl: URL) -> [URL] {
var files: [URL] = []

let resourceKeys: [URLResourceKey] = [.creationDateKey, .isRegularFileKey]
let enumerator = FileManager.default.enumerator(
at: currentUrl,
includingPropertiesForKeys: resourceKeys
)!

for case let fileUrl as URL in enumerator {
// Rationale: force-try is ok. This can never fail, as the resourceKeys passed here are also passed to the enumerator
// swiftlint:disable:next force_try
let resourceValues = try! fileUrl.resourceValues(forKeys: Set(resourceKeys))
// Force-unwrap is ok: this can never fail, as the isRegularFileKey resource key is passed previously to the enumerator
if resourceValues.isRegularFile! {
let url = URL(fileURLWithPath: fileUrl.path.replacingOccurrences(of: "\(currentUrl.path)/", with: ""), relativeTo: currentUrl)
files.append(url)
}
}

return files
}
}
12 changes: 12 additions & 0 deletions Tests/ProjLintKitTests/Globals/ResourceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import XCTest

final class ResourceTest: XCTestCase {
private let resource = Resource(
path: "directory/Resource.swift",
contents: ""
)

func testRelativePath() {
XCTAssertEqual(resource.relativePath, "directory/Resource.swift")
}
}
69 changes: 69 additions & 0 deletions Tests/ProjLintKitTests/Rules/FileExistenceRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,73 @@ final class FileExistenceRuleTests: XCTestCase {
XCTAssert(violations.isEmpty)
}
}

func testAllowedPathsWithValidRegex() {
resourcesLoaded([infoPlistResource]) {
let optionsDict = ["allowed_paths_regex": [#"Sources/SuportingFiles/Info\.plist"#]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 0)
}

resourcesLoaded([infoPlistResource]) {
let optionsDict = ["allowed_paths_regex": [#"Sources/SuportingFiles/Info2\.plist"#]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations.compactMap { ($0 as? FileViolation)?.url.relativePath }, [infoPlistResource.relativePath])
XCTAssertEqual(violations.compactMap { ($0 as? FileViolation)?.message }, ["File exists, but it mustn\'t."])
}

resourcesLoaded([infoPlistResource]) {
let optionsDict = ["allowed_paths_regex": [#"Sources/SuportingFiles/.*"#]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 0)
}

resourcesLoaded([infoPlistResource]) {
let optionsDict = ["allowed_paths_regex": [#".*"#]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 0)
}

resourcesLoaded([infoPlistResource]) {
let optionsDict = ["allowed_paths_regex": [#".*\.png"#]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations.compactMap { ($0 as? FileViolation)?.url.relativePath }, [infoPlistResource.relativePath])
XCTAssertEqual(violations.compactMap { ($0 as? FileViolation)?.message }, ["File exists, but it mustn\'t."])
}
}

func testAllowedPathsWithInvalidRegex() {
let invalidRegex = #"["#
let optionsDict = ["allowed_paths_regex": [invalidRegex]]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations.map { $0.message }, ["The following regex is not valid: \'[\'"])
}

func testAllowedPathsWithSamePathInOtherRules() {
resourcesLoaded([infoPlistResource]) {
let optionsDict = [
"existing_paths": [infoPlistResource.relativePath],
"allowed_paths_regex": [#"ThisIsARandomPathThatDoesNotMatch"#]
]
let rule = FileExistenceRule(optionsDict)

let violations = rule.violations(in: Resource.baseUrl)
XCTAssertEqual(violations.count, 0)
}
}
}