From 5a8dfcef3c18b200fa4e7287959b25e22180be20 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Mon, 1 Dec 2025 20:22:24 -0800 Subject: [PATCH 01/15] Fix Windows path handling in various places An assortment of fixes that improve the test coverage on Windows. With this set, local testing reveals 1 failure (RemoteCycleDetection) and another set of test failures due to Windows globbing not matching the POSIX semantics. Co-authored-by: Roman Lavrov --- Source/SwiftLintCore/Models/Baseline.swift | 4 +++- .../Configuration+CommandLine.swift | 2 +- .../Configuration/Configuration+Cache.swift | 13 ++++++------- Source/swiftlint/Commands/Docs.swift | 4 ++++ .../BuiltInRulesTests/FileHeaderRuleTests.swift | 2 +- .../FileNameNoSpaceRuleTests.swift | 2 +- Tests/BuiltInRulesTests/FileNameRuleTests.swift | 3 ++- Tests/CoreTests/YamlSwiftLintTests.swift | 3 ++- .../ConfigurationTests+Mock.swift | 3 ++- Tests/FileSystemAccessTests/GlobTests.swift | 2 +- Tests/FileSystemAccessTests/ReporterTests.swift | 17 ++++++++++++----- .../SourceKitCrashTests.swift | 2 +- Tests/FrameworkTests/CustomRulesTests.swift | 2 +- Tests/TestHelpers/TestResources.swift | 9 +++++---- 14 files changed, 42 insertions(+), 26 deletions(-) diff --git a/Source/SwiftLintCore/Models/Baseline.swift b/Source/SwiftLintCore/Models/Baseline.swift index 4e850db5ea..3c8f9e56ec 100644 --- a/Source/SwiftLintCore/Models/Baseline.swift +++ b/Source/SwiftLintCore/Models/Baseline.swift @@ -192,7 +192,9 @@ private extension StyleViolation { var withAbsolutePath: StyleViolation { let absolutePath: String? = if let relativePath = location.file { - FileManager.default.currentDirectoryPath + "/" + relativePath + URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent(relativePath) + .filepath } else { nil } diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index f2871b4cd8..4776964231 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -138,7 +138,7 @@ extension Configuration { pathComponents.removeFirst() } - return pathComponents.joined(separator: "/") + return pathComponents.reduce(URL(fileURLWithPath: "/")) { $0.appendingPathComponent($1) }.filepath } private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]], diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift b/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift index 47bf254fe7..a8294914c7 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift @@ -81,13 +81,12 @@ extension Configuration { #endif } - let versionedDirectory = [ - "SwiftLint", - Version.current.value, - ExecutableInfo.buildID, - ].compactMap(\.self).joined(separator: "/") - - let folder = baseURL.appendingPathComponent(versionedDirectory) + var folder = baseURL + .appendingPathComponent("SwiftLint") + .appendingPathComponent(Version.current.value) + if let buildID = ExecutableInfo.buildID { + folder = folder.appendingPathComponent(buildID) + } do { try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil) diff --git a/Source/swiftlint/Commands/Docs.swift b/Source/swiftlint/Commands/Docs.swift index 8b3d368e3a..c9c4fa90f4 100644 --- a/Source/swiftlint/Commands/Docs.swift +++ b/Source/swiftlint/Commands/Docs.swift @@ -33,6 +33,10 @@ private func open(_ url: URL) { let command = "xdg-open" process.arguments = [command, url.absoluteString] try? process.run() +#elseif os(Windows) + process.executableURL = URL(fileURLWithPath: "cmd", isDirectory: false) + process.arguments = ["/C", "start", url.absoluteString] + try? process.run() #else process.launchPath = "/usr/bin/env" let command = "open" diff --git a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift index 9d38e96843..3ce1b5e6b1 100644 --- a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift @@ -2,7 +2,7 @@ import TestHelpers import XCTest -private let fixturesDirectory = "\(TestResources.path())/FileHeaderRuleFixtures" +private let fixturesDirectory = "\(TestResources.path().filepath)/FileHeaderRuleFixtures" final class FileHeaderRuleTests: SwiftLintTestCase { private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] { diff --git a/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift b/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift index 9c0b3a1d01..1d142807c1 100644 --- a/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift @@ -3,7 +3,7 @@ import SourceKittenFramework import TestHelpers import XCTest -private let fixturesDirectory = "\(TestResources.path())/FileNameNoSpaceRuleFixtures" +private let fixturesDirectory = "\(TestResources.path().filepath)/FileNameNoSpaceRuleFixtures" final class FileNameNoSpaceRuleTests: SwiftLintTestCase { private func validate(fileName: String, excludedOverride: [String]? = nil) throws -> [StyleViolation] { diff --git a/Tests/BuiltInRulesTests/FileNameRuleTests.swift b/Tests/BuiltInRulesTests/FileNameRuleTests.swift index d81ede7ffa..80c6db6d2a 100644 --- a/Tests/BuiltInRulesTests/FileNameRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameRuleTests.swift @@ -2,7 +2,8 @@ import TestHelpers import XCTest -private let fixturesDirectory = "\(TestResources.path())/FileNameRuleFixtures" +private let fixturesDirectory = + TestResources.path().appendingPathComponent("FileNameRuleFixtures").filepath final class FileNameRuleTests: SwiftLintTestCase { private func validate(fileName: String, diff --git a/Tests/CoreTests/YamlSwiftLintTests.swift b/Tests/CoreTests/YamlSwiftLintTests.swift index aacc3232d7..89dcef9707 100644 --- a/Tests/CoreTests/YamlSwiftLintTests.swift +++ b/Tests/CoreTests/YamlSwiftLintTests.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftLintCore import TestHelpers import XCTest import Yams @@ -40,6 +41,6 @@ final class YamlSwiftLintTests: SwiftLintTestCase { } private func getTestYaml() throws -> String { - try String(contentsOfFile: "\(TestResources.path())/test.yml", encoding: .utf8) + try String(contentsOfFile: "\(TestResources.path().filepath)/test.yml", encoding: .utf8) } } diff --git a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift index e4a7fc5e15..796f31916e 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift @@ -1,3 +1,4 @@ +import Foundation import SwiftLintFramework import TestHelpers @@ -7,7 +8,7 @@ import TestHelpers internal extension ConfigurationTests { enum Mock { // MARK: Test Resources Path - static let testResourcesPath: String = TestResources.path() + static let testResourcesPath: String = TestResources.path().filepath // MARK: Directory Paths enum Dir { diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index ec8836f2a1..2f763e9f0d 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -6,7 +6,7 @@ import XCTest final class GlobTests: SwiftLintTestCase { private var mockPath: String { - TestResources.path().stringByAppendingPathComponent("ProjectMock") + TestResources.path().appendingPathComponent("ProjectMock").filepath } func testNonExistingDirectory() { diff --git a/Tests/FileSystemAccessTests/ReporterTests.swift b/Tests/FileSystemAccessTests/ReporterTests.swift index cebf5082a9..85bd0090a8 100644 --- a/Tests/FileSystemAccessTests/ReporterTests.swift +++ b/Tests/FileSystemAccessTests/ReporterTests.swift @@ -23,7 +23,10 @@ final class ReporterTests: SwiftLintTestCase { ruleDescription: SyntacticSugarRule.description, severity: .error, location: Location( - file: FileManager.default.currentDirectoryPath + "/path/file.swift", + file: URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent("path") + .appendingPathComponent("file.swift") + .path, line: 1, character: 2 ), @@ -43,7 +46,8 @@ final class ReporterTests: SwiftLintTestCase { } private func stringFromFile(_ filename: String) -> String { - SwiftLintFile(path: "\(TestResources.path())/\(filename)")!.contents + let path = TestResources.path().appendingPathComponent(filename) + return SwiftLintFile(path: path.filepath)!.contents } func testXcodeReporter() throws { @@ -231,9 +235,12 @@ final class ReporterTests: SwiftLintTestCase { line: UInt = #line) throws { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short + + let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).path + let reference = stringFromFile(referenceFile).replacingOccurrences( of: "${CURRENT_WORKING_DIRECTORY}", - with: FileManager.default.currentDirectoryPath + with: cwd ).replacingOccurrences( of: "${SWIFTLINT_VERSION}", with: SwiftLintFramework.Version.current.value @@ -245,9 +252,9 @@ final class ReporterTests: SwiftLintTestCase { let convertedReference = try stringConverter(reference) let convertedReporterOutput = try stringConverter(reporterOutput) if convertedReference != convertedReporterOutput { - let referenceURL = URL(fileURLWithPath: "\(TestResources.path())/\(referenceFile)") + let referenceURL = TestResources.path().appendingPathComponent(referenceFile) try reporterOutput.replacingOccurrences( - of: FileManager.default.currentDirectoryPath, + of: cwd, with: "${CURRENT_WORKING_DIRECTORY}" ).replacingOccurrences( of: SwiftLintFramework.Version.current.value, diff --git a/Tests/FileSystemAccessTests/SourceKitCrashTests.swift b/Tests/FileSystemAccessTests/SourceKitCrashTests.swift index 34c00d65cb..db6fbfdd54 100644 --- a/Tests/FileSystemAccessTests/SourceKitCrashTests.swift +++ b/Tests/FileSystemAccessTests/SourceKitCrashTests.swift @@ -35,7 +35,7 @@ final class SourceKitCrashTests: SwiftLintTestCase { } func testRulesWithFileThatCrashedSourceKitService() throws { - let file = try XCTUnwrap(SwiftLintFile(path: "\(TestResources.path())/ProjectMock/Level0.swift")) + let file = try XCTUnwrap(SwiftLintFile(path: "\(TestResources.path().filepath)/ProjectMock/Level0.swift")) file.sourcekitdFailed = true file.assertHandler = { XCTFail("If this called, rule's SourceKitFreeRule is not properly configured") diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 051a304d60..729f9c8609 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -9,7 +9,7 @@ import XCTest final class CustomRulesTests: SwiftLintTestCase { private typealias Configuration = RegexConfiguration - private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path())/test.txt")! } + private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path().filepath)/test.txt")! } override func invokeTest() { CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { diff --git a/Tests/TestHelpers/TestResources.swift b/Tests/TestHelpers/TestResources.swift index 58a7f13bb0..d154988bf7 100644 --- a/Tests/TestHelpers/TestResources.swift +++ b/Tests/TestHelpers/TestResources.swift @@ -2,14 +2,15 @@ import Foundation import SwiftLintCore public enum TestResources { - public static func path(_ calleePath: String = #filePath) -> String { + public static func path(_ calleePath: String = #filePath) -> URL { let folder = URL(fileURLWithPath: calleePath, isDirectory: false).deletingLastPathComponent() if let rootProjectDirectory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] { - return "\(rootProjectDirectory)/Tests/\(folder.lastPathComponent)/Resources" + return URL( + fileURLWithPath: "\(rootProjectDirectory)/Tests/\(folder.lastPathComponent)/Resources", + isDirectory: true) } return folder .appendingPathComponent("Resources") - .path - .absolutePathStandardized() + .resolvingSymlinksInPath() } } From fe9e1d5abea8df598fcc53396fc61ec42d50c4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 10 Jan 2026 20:10:58 +0100 Subject: [PATCH 02/15] Use URL for file paths throughout the code base # Conflicts: # Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift # Tests/IntegrationTests/ConfigPathResolutionTests.swift --- .../Rules/Idiomatic/FileNameNoSpaceRule.swift | 2 +- .../Rules/Idiomatic/FileNameRule.swift | 2 +- .../Lint/BlanketDisableCommandRule.swift | 2 +- .../Rules/Lint/CaptureVariableRule.swift | 2 +- .../Rules/Lint/TypesafeArrayInitRule.swift | 2 +- .../Rules/Lint/UnusedDeclarationRule.swift | 4 +- .../Rules/Lint/UnusedImportRule.swift | 6 +- .../FileHeaderConfiguration.swift | 2 +- .../FileNameConfiguration.swift | 7 +- .../Rules/Style/ExplicitSelfRule.swift | 5 +- .../Rules/Style/FileTypesOrderRule.swift | 3 +- .../Extensions/String+SwiftLint.swift | 13 +- .../Extensions/SwiftLintFile+Cache.swift | 2 +- .../Extensions/SwiftLintFile+Regex.swift | 4 +- .../Extensions/URL+SwiftLint.swift | 76 +++++++- Source/SwiftLintCore/Models/Baseline.swift | 49 ++--- Source/SwiftLintCore/Models/Correction.swift | 8 +- Source/SwiftLintCore/Models/Issue.swift | 28 +-- Source/SwiftLintCore/Models/Location.swift | 43 ++--- .../SwiftLintCore/Models/SwiftLintFile.swift | 20 +- .../RegexConfiguration.swift | 9 +- Source/SwiftLintFramework/Benchmark.swift | 4 +- .../CompilerArgumentsExtractor.swift | 2 +- .../Configuration+CommandLine.swift | 47 ++--- .../Configuration/Configuration+Cache.swift | 12 +- .../Configuration+FileGraph.swift | 39 ++-- .../Configuration+FileGraphSubtypes.swift | 9 +- .../Configuration+LintableFiles.swift | 45 +++-- .../Configuration/Configuration+Merging.swift | 58 ++---- .../Configuration/Configuration+Parsing.swift | 14 +- .../Configuration/Configuration.swift | 49 +++-- .../Documentation/RuleListDocumentation.swift | 2 +- .../Extensions/FileManager+SwiftLint.swift | 67 ++++--- Source/SwiftLintFramework/Helpers/Glob.swift | 52 ++---- .../LintOrAnalyzeCommand.swift | 27 ++- .../LintableFilesVisitor.swift | 23 +-- Source/SwiftLintFramework/Models/Linter.swift | 5 +- .../Models/LinterCache.swift | 17 +- .../Reporters/CSVReporter.swift | 2 +- .../Reporters/CheckstyleReporter.swift | 4 +- .../Reporters/CodeClimateReporter.swift | 14 +- .../Reporters/EmojiReporter.swift | 6 +- .../GitHubActionsLoggingReporter.swift | 2 +- .../Reporters/GitLabJUnitReporter.swift | 2 +- .../Reporters/HTMLReporter.swift | 2 +- .../Reporters/JSONReporter.swift | 2 +- .../Reporters/JUnitReporter.swift | 2 +- .../Reporters/MarkdownReporter.swift | 2 +- .../Reporters/RelativePathReporter.swift | 2 +- .../Reporters/SARIFReporter.swift | 4 +- .../Reporters/SonarQubeReporter.swift | 2 +- Source/swiftlint-dev/Reporters+Register.swift | 12 +- Source/swiftlint-dev/Rules+Register.swift | 36 ++-- Source/swiftlint-dev/Rules+Template.swift | 22 +-- Source/swiftlint/Commands/Analyze.swift | 5 +- Source/swiftlint/Commands/Baseline.swift | 10 +- Source/swiftlint/Commands/Docs.swift | 4 +- Source/swiftlint/Commands/GenerateDocs.swift | 4 +- Source/swiftlint/Commands/Lint.swift | 5 +- Source/swiftlint/Commands/Rules.swift | 2 +- .../Common/LintOrAnalyzeArguments.swift | 14 +- .../FileHeaderRuleTests.swift | 8 +- .../FileNameNoSpaceRuleTests.swift | 21 +-- .../BuiltInRulesTests/FileNameRuleTests.swift | 5 +- Tests/CoreTests/RegexConfigurationTests.swift | 46 ++--- Tests/CoreTests/SwiftLintFileTests.swift | 16 +- Tests/CoreTests/YamlSwiftLintTests.swift | 5 +- .../FileSystemAccessTests/BaselineTests.swift | 37 ++-- .../ConfigurationTests+Mock.swift | 148 ++++++++------- .../ConfigurationTests.swift | 172 +++++++++--------- Tests/FileSystemAccessTests/GlobTests.swift | 87 +++++---- ...wift => MultipleConfigurationsTests.swift} | 81 ++++++--- .../FileSystemAccessTests/ReporterTests.swift | 34 ++-- .../Resources/CannedCSVReporterOutput.csv | 4 +- .../CannedCheckstyleReporterOutput.xml | 8 +- .../CannedCodeClimateReporterOutput.json | 8 +- .../Resources/CannedEmojiReporterOutput.txt | 8 +- .../Resources/CannedJSONReporterOutput.json | 6 +- .../Resources/CannedJunitReporterOutput.xml | 4 +- .../Resources/CannedMarkdownReporterOutput.md | 4 +- .../Resources/CannedXcodeReporterOutput.txt | 4 +- .../Resources/ProjectMock/Baseline.json | 1 + .../SourceKitCrashTests.swift | 5 +- Tests/FrameworkTests/CustomRulesTests.swift | 10 +- Tests/FrameworkTests/LinterCacheTests.swift | 35 ++-- .../ConfigPathResolutionTests.swift | 26 +-- Tests/IntegrationTests/IntegrationTests.swift | 10 +- Tests/TestHelpers/TestHelpers.swift | 42 ++--- Tests/TestHelpers/TestResources.swift | 11 +- 89 files changed, 900 insertions(+), 867 deletions(-) rename Tests/FileSystemAccessTests/{ConfigurationTests+MultipleConfigs.swift => MultipleConfigurationsTests.swift} (93%) create mode 100644 Tests/FileSystemAccessTests/Resources/ProjectMock/Baseline.json diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift index 0a5d60e549..54e860f3db 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift @@ -13,7 +13,7 @@ struct FileNameNoSpaceRule: OptInRule, SourceKitFreeRule { func validate(file: SwiftLintFile) -> [StyleViolation] { guard let filePath = file.path, - case let fileName = filePath.bridge().lastPathComponent, + case let fileName = filePath.lastPathComponent, !configuration.excluded.contains(fileName), fileName.rangeOfCharacter(from: .whitespaces) != nil else { return [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift index 393c37bfe0..ab02d9b1fd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift @@ -21,7 +21,7 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))") let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z") - let fileName = filePath.bridge().lastPathComponent + let fileName = filePath.lastPathComponent var typeInFileName = fileName.bridge().deletingPathExtension // Process prefix diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift index 7179e0947b..9c45c00e0f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift @@ -221,6 +221,6 @@ private extension Command { location = line.distance(from: line.startIndex, to: ruleIdentifierIndex) + 1 } } - return Location(file: file.file.path, line: line, character: location) + return Location(file: file.path, line: line, character: location) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift index 92871e1448..de2699a925 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift @@ -262,7 +262,7 @@ private extension SwiftLintFile { func index(compilerArguments: [String]) -> SourceKittenDictionary? { guard let path, - let response = try? Request.index(file: path, arguments: compilerArguments).sendIfNotDisabled() + let response = try? Request.index(file: path.filepath, arguments: compilerArguments).sendIfNotDisabled() else { Issue.indexingError(path: path, ruleID: CaptureVariableRule.identifier).print() return nil diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift index db265b8a1f..1d55d5bc36 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift @@ -76,7 +76,7 @@ struct TypesafeArrayInitRule: AnalyzerRule { return false } let cursorInfo = Request.cursorInfoWithoutSymbolGraph( - file: filePath, offset: offset, arguments: compilerArguments + file: filePath.filepath, offset: offset, arguments: compilerArguments ) guard let request = try? cursorInfo.sendIfNotDisabled() else { return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index 4813c58132..e6e97990ff 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -84,7 +84,7 @@ private extension SwiftLintFile { func index(compilerArguments: [String]) -> SourceKittenDictionary? { path .flatMap { path in - try? Request.index(file: path, arguments: compilerArguments).send() + try? Request.index(file: path.filepath, arguments: compilerArguments).send() } .map(SourceKittenDictionary.init) } @@ -203,7 +203,7 @@ private extension SwiftLintFile { func cursorInfo(at byteOffset: ByteCount, compilerArguments: [String]) -> SourceKittenDictionary? { let request = Request.cursorInfoWithoutSymbolGraph( - file: path!, offset: byteOffset, arguments: compilerArguments + file: path!.filepath, offset: byteOffset, arguments: compilerArguments ) return (try? request.sendIfNotDisabled()).map(SourceKittenDictionary.init) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift index a6cde07e47..64bfca0447 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift @@ -171,7 +171,7 @@ private extension SwiftLintFile { continue } let cursorInfoRequest = Request.cursorInfoWithoutSymbolGraph( - file: path!, offset: token.offset, arguments: compilerArguments + file: path!.filepath, offset: token.offset, arguments: compilerArguments ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()).map(SourceKittenDictionary.init) else { Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.identifier).print() @@ -211,7 +211,7 @@ private extension SwiftLintFile { // Operators are omitted in the editor.open request and thus have to be looked up by the indexsource request func operatorImports(arguments: [String], processedTokenOffsets: Set) -> Set { - guard let index = (try? Request.index(file: path!, arguments: arguments).sendIfNotDisabled()) + guard let index = (try? Request.index(file: path!.filepath, arguments: arguments).sendIfNotDisabled()) .map(SourceKittenDictionary.init) else { Issue.indexingError(path: path, ruleID: UnusedImportRule.identifier).print() return [] @@ -231,7 +231,7 @@ private extension SwiftLintFile { guard !processedTokenOffsets.contains(ByteCount(offset)) else { continue } let cursorInfoRequest = Request.cursorInfoWithoutSymbolGraph( - file: path!, offset: ByteCount(offset), arguments: arguments + file: path!.filepath, offset: ByteCount(offset), arguments: arguments ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()) .map(SourceKittenDictionary.init) else { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift index 8bdd523320..0648074ecd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift @@ -77,7 +77,7 @@ struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { escapeFileName: Bool) -> NSRegularExpression? { // Recompile the regex for this file... let replacedPattern = file.path.map { path in - let fileName = path.bridge().lastPathComponent + let fileName = path.lastPathComponent // Replace SWIFTLINT_CURRENT_FILENAME with the filename. let escapedName = escapeFileName ? NSRegularExpression.escapedPattern(for: fileName) : fileName diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift index 2890ec5f36..1a91785a61 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift @@ -21,13 +21,12 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { } extension FileNameConfiguration { - func shouldExclude(filePath: String) -> Bool { - let fileName = filePath.bridge().lastPathComponent - if excluded.contains(fileName) { + func shouldExclude(filePath: URL) -> Bool { + if excluded.contains(filePath.lastPathComponent) { return true } return excludedPaths.contains { - $0.regex.firstMatch(in: filePath, range: filePath.fullNSRange) != nil + $0.regex.firstMatch(in: filePath.path, range: filePath.path.fullNSRange) != nil } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift index a85c4a5a45..5371c143c0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift @@ -86,7 +86,7 @@ private extension SwiftLintFile { try byteOffsets.compactMap { offset in if isExplicitAccess(at: offset) { return nil } let cursorInfoRequest = Request.cursorInfoWithoutSymbolGraph( - file: self.path!, offset: offset, arguments: compilerArguments + file: path!.filepath, offset: offset, arguments: compilerArguments ) var cursorInfo = try cursorInfoRequest.sendIfNotDisabled() @@ -135,8 +135,7 @@ private extension StringView { } private func binaryOffsets(file: SwiftLintFile, compilerArguments: [String]) throws -> [ByteCount] { - let absoluteFile = file.path!.bridge().absolutePathRepresentation() - let index = try Request.index(file: absoluteFile, arguments: compilerArguments).sendIfNotDisabled() + let index = try Request.index(file: file.path!.filepath, arguments: compilerArguments).sendIfNotDisabled() let binaryOffsets = file.stringView.recursiveByteOffsets(index) return binaryOffsets.sorted() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift index 7b26c687ec..ba78e89354 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift @@ -136,8 +136,7 @@ struct FileTypesOrderRule: OptInRule { return mainTypeSubstructure(in: dict) } - let fileName = URL(fileURLWithPath: filePath, isDirectory: false) - .lastPathComponent.replacingOccurrences(of: ".swift", with: "") + let fileName = filePath.lastPathComponent.replacingOccurrences(of: ".swift", with: "") guard let mainTypeSubstructure = dict.substructure.first(where: { $0.name == fileName }) else { return mainTypeSubstructure(in: file.structureDictionary) } diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index 24fb25e3de..b6b7e6dfa5 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -73,18 +73,7 @@ public extension String { /// /// - returns: A new `String`. func absolutePathStandardized() -> String { - URL(fileURLWithPath: bridge().standardizingPath.absolutePathRepresentation()).filepath - } - - var isFile: Bool { - if isEmpty { - return false - } - var isDirectoryObjC: ObjCBool = false - if FileManager.default.fileExists(atPath: self, isDirectory: &isDirectoryObjC) { - return !isDirectoryObjC.boolValue - } - return false + URL(filePath: bridge().standardizingPath.absolutePathRepresentation()).filepath } /// Count the number of occurrences of the given character in `self` diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift index b22499747b..6ad479b8c1 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift @@ -34,7 +34,7 @@ private let foldedSyntaxTreeCache = Cache { file -> SourceFileSyntax? in .as(SourceFileSyntax.self) } private let locationConverterCache = Cache { file -> SourceLocationConverter in - SourceLocationConverter(fileName: file.path ?? "", tree: file.syntaxTree) + SourceLocationConverter(fileName: file.path?.filepath ?? "", tree: file.syntaxTree) } private let commandsCache = Cache { file -> [Command] in guard file.contents.contains("swiftlint:") else { diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift index 275ef16cb9..7c96561d0d 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift @@ -142,7 +142,7 @@ extension SwiftLintFile { guard let stringData = string.data(using: .utf8) else { queuedFatalError("can't encode '\(string)' with UTF8") } - guard let path, let fileHandle = FileHandle(forWritingAtPath: path) else { + guard let path, let fileHandle = FileHandle(forWritingAtPath: path.filepath) else { queuedFatalError("can't write to path '\(String(describing: path))'") } _ = fileHandle.seekToEndOfFile() @@ -166,7 +166,7 @@ extension SwiftLintFile { queuedFatalError("can't encode '\(string)' with UTF8") } do { - try stringData.write(to: URL(fileURLWithPath: path, isDirectory: false), options: .atomic) + try stringData.write(to: path, options: .atomic) } catch { queuedFatalError("can't write file to \(path)") } diff --git a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift index b6781612e7..e2343c6c98 100644 --- a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift @@ -1,6 +1,10 @@ import Foundation public extension URL { + static var cwd: URL { + FileManager.default.currentDirectoryPath.url(directoryHint: .isDirectory) + } + var filepath: String { withUnsafeFileSystemRepresentation { String(cString: $0!) } } @@ -18,6 +22,76 @@ public extension URL { } var isSwiftFile: Bool { - filepath.isFile && pathExtension == "swift" + isFile && pathExtension == "swift" + } + + var isFile: Bool { + var isDirectoryObjC: ObjCBool = false + if FileManager.default.fileExists(atPath: filepath, isDirectory: &isDirectoryObjC) { + return !isDirectoryObjC.boolValue + } + return false + } + + var isDirectory: Bool { + var isDirectoryObjC: ObjCBool = false + if FileManager.default.fileExists(atPath: filepath, isDirectory: &isDirectoryObjC) { + return isDirectoryObjC.boolValue + } + return false + } + + /// Path relative to the current working directory. + /// + /// > Warning: Use this representation only for displaying file paths to users. It is not + /// suitable for file operations. + var relativeFilepath: String { + let path = path.replacing(URL.cwd.path, with: "") + if path.starts(with: "/") { + return String(path.dropFirst()) + } + return path + } + + var exists: Bool { + isFileURL && FileManager.default.fileExists(atPath: filepath) + } + + func relative(to base: URL) -> URL { + guard base.isFileURL, isFileURL else { + return self + } + + let baseComponents = base.standardizedFileURL.pathComponents + let selfComponents = standardizedFileURL.pathComponents + + var index = 0 + while index < baseComponents.count, index < selfComponents.count, + baseComponents[index] == selfComponents[index] { + index += 1 + } + + var newPath = base + for _ in index.. URL { + guard var base else { + return URL(filePath: self, directoryHint: directoryHint).standardizedFileURL + } + if base.isDirectory { + let lastComponent = base.lastPathComponent + base.deleteLastPathComponent() + base.append(path: lastComponent, directoryHint: .isDirectory) + } + return URL(filePath: self, directoryHint: directoryHint, relativeTo: base).standardizedFileURL } } diff --git a/Source/SwiftLintCore/Models/Baseline.swift b/Source/SwiftLintCore/Models/Baseline.swift index 3c8f9e56ec..2f417508a7 100644 --- a/Source/SwiftLintCore/Models/Baseline.swift +++ b/Source/SwiftLintCore/Models/Baseline.swift @@ -11,14 +11,7 @@ private struct BaselineViolation: Codable, Hashable, Comparable { var key: String { text + violation.reason } init(violation: StyleViolation, text: String) { - let location = violation.location - self.violation = violation.with(location: Location( - // Within the baseline, we use relative paths, so that - // comparisons are independent of the absolute path - file: location.relativeFile, - line: location.line, - character: location.character) - ) + self.violation = violation self.text = text } @@ -42,14 +35,14 @@ public struct Baseline: Equatable { /// The stored violations. public var violations: [StyleViolation] { - sortedBaselineViolations.violationsWithAbsolutePaths + sortedBaselineViolations.map(\.violation) } /// Creates a `Baseline` from a saved file. /// /// - parameter fromPath: The path to read from. - public init(fromPath path: String) throws { - let data = try Data(contentsOf: URL(fileURLWithPath: path)) + public init(fromPath path: URL) throws { + let data = try Data(contentsOf: path) baseline = try JSONDecoder().decode(BaselineViolations.self, from: data).groupedByFile() } @@ -63,9 +56,8 @@ public struct Baseline: Equatable { /// Writes a `Baseline` to disk in JSON format. /// /// - parameter toPath: The path to write to. - public func write(toPath path: String) throws { - let data = try JSONEncoder().encode(sortedBaselineViolations) - try data.write(to: URL(fileURLWithPath: path)) + public func write(toPath path: URL) throws { + try JSONEncoder().encode(sortedBaselineViolations).write(to: path) } /// Filters out violations that are present in the `Baseline`. @@ -76,7 +68,7 @@ public struct Baseline: Equatable { /// - Returns: The new violations. public func filter(_ violations: [StyleViolation]) -> [StyleViolation] { guard let firstViolation = violations.first, - let baselineViolations = baseline[firstViolation.location.relativeFile ?? ""], + let baselineViolations = baseline[firstViolation.location.file?.relativeFilepath ?? ""], baselineViolations.isNotEmpty else { return violations } @@ -126,7 +118,7 @@ public struct Baseline: Equatable { } } - return Set(filteredViolations.violationsWithAbsolutePaths) + return Set(filteredViolations.map(\.violation)) } /// Returns the violations that are present in another `Baseline`, but not in this one. @@ -139,7 +131,7 @@ public struct Baseline: Equatable { if let baselineViolations = baseline[relativePath] { return filter(relativePathViolations: otherBaselineViolations, baselineViolations: baselineViolations) } - return Set(otherBaselineViolations.violationsWithAbsolutePaths) + return Set(otherBaselineViolations.map(\.violation)) }.sorted { $0.location == $1.location ? $0.ruleIdentifier < $1.ruleIdentifier : $0.location < $1.location } @@ -147,7 +139,7 @@ public struct Baseline: Equatable { } private struct LineCache { - private var lines: [String: [String]] = [:] + private var lines: [URL: [String]] = [:] mutating func text(at location: Location) -> String { let line = (location.line ?? 0) - 1 @@ -157,7 +149,7 @@ private struct LineCache { return "" } - private mutating func cached(file: String) -> [String]? { + private mutating func cached(file: URL) -> [String]? { if let fileLines = lines[file] { return fileLines } @@ -175,12 +167,8 @@ private extension Sequence where Element == BaselineViolation { self = violations.map { $0.baselineViolation(text: lineCache.text(at: $0.location)) } } - var violationsWithAbsolutePaths: [StyleViolation] { - map(\.violation.withAbsolutePath) - } - func groupedByFile() -> ViolationsPerFile { - Dictionary(grouping: self) { $0.violation.location.relativeFile ?? "" } + Dictionary(grouping: self) { $0.violation.location.file?.relativeFilepath ?? "" } } func groupedByRuleIdentifier(filteredBy existingViolations: [BaselineViolation] = []) -> ViolationsPerRule { @@ -189,19 +177,6 @@ private extension Sequence where Element == BaselineViolation { } private extension StyleViolation { - var withAbsolutePath: StyleViolation { - let absolutePath: String? = - if let relativePath = location.file { - URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(relativePath) - .filepath - } else { - nil - } - let newLocation = Location(file: absolutePath, line: location.line, character: location.character) - return with(location: newLocation) - } - func baselineViolation(text: String = "") -> BaselineViolation { BaselineViolation(violation: self, text: text) } diff --git a/Source/SwiftLintCore/Models/Correction.swift b/Source/SwiftLintCore/Models/Correction.swift index ef1eb206f3..91d815830e 100644 --- a/Source/SwiftLintCore/Models/Correction.swift +++ b/Source/SwiftLintCore/Models/Correction.swift @@ -1,16 +1,18 @@ +import Foundation + /// A value describing a SwiftLint violation that was corrected. public struct Correction: Equatable, Sendable { /// The name of the rule that was corrected. public let ruleName: String /// The path to the file that was corrected. - public let filePath: String? + public let filePath: URL? /// The number of corrections that were made. public let numberOfCorrections: Int /// The console-printable description for this correction. public var consoleDescription: String { let times = numberOfCorrections == 1 ? "time" : "times" - return "\(filePath ?? ""): Corrected \(ruleName) \(numberOfCorrections) \(times)" + return "\(filePath?.relativeFilepath ?? ""): Corrected \(ruleName) \(numberOfCorrections) \(times)" } /// Memberwise initializer. @@ -18,7 +20,7 @@ public struct Correction: Equatable, Sendable { /// - parameter ruleName: The name of the rule that was corrected. /// - parameter filePath: The path to the file that was corrected. /// - parameter numberOfCorrections: The number of corrections that were made. - public init(ruleName: String, filePath: String?, numberOfCorrections: Int) { + public init(ruleName: String, filePath: URL?, numberOfCorrections: Int) { self.ruleName = ruleName self.filePath = filePath self.numberOfCorrections = numberOfCorrections diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index 86e0190b34..849344c9ec 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -55,31 +55,31 @@ public enum Issue: LocalizedError, Equatable { case ruleDeprecated(ruleID: String) /// The initial configuration file was not found. - case initialFileNotFound(path: String) + case initialFileNotFound(path: URL) /// A file at specified path was not found. - case fileNotFound(path: String) + case fileNotFound(path: URL) /// The file at `path` is not readable or cannot be opened. - case fileNotReadable(path: String?, ruleID: String) + case fileNotReadable(path: URL?, ruleID: String) /// The file at `path` is not writable. - case fileNotWritable(path: String) + case fileNotWritable(path: URL) /// The file at `path` cannot be indexed by a specific rule. - case indexingError(path: String?, ruleID: String) + case indexingError(path: URL?, ruleID: String) /// No arguments were provided to compile a file at `path` within a specific rule. - case missingCompilerArguments(path: String?, ruleID: String) + case missingCompilerArguments(path: URL?, ruleID: String) /// Cursor information cannot be extracted from a specific location. - case missingCursorInfo(path: String?, ruleID: String) + case missingCursorInfo(path: URL?, ruleID: String) /// An error that occurred when parsing YAML. case yamlParsing(String) /// The baseline file at `path` is not readable or cannot be opened. - case baselineNotReadable(path: String) + case baselineNotReadable(path: URL) /// Flag to enable warnings for deprecations being printed to the console. Printing is enabled by default. package nonisolated(unsafe) static var printDeprecationWarnings = true @@ -183,22 +183,22 @@ public enum Issue: LocalizedError, Equatable { case let .fileNotFound(path): return "File at path '\(path)' not found." case let .fileNotReadable(path, id): - return "Cannot open or read file at path '\(path ?? "...")' within '\(id)' rule." + return "Cannot open or read file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." case let .fileNotWritable(path): - return "Cannot write to file at path '\(path)'." + return "Cannot write to file at path '\(path.relativeFilepath)'." case let .indexingError(path, id): - return "Cannot index file at path '\(path ?? "...")' within '\(id)' rule." + return "Cannot index file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." case let .missingCompilerArguments(path, id): return """ - Attempted to lint file at path '\(path ?? "...")' within '\(id)' rule \ + Attempted to lint file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule \ without any compiler arguments. """ case let .missingCursorInfo(path, id): - return "Cannot get cursor info from file at path '\(path ?? "...")' within '\(id)' rule." + return "Cannot get cursor info from file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." case let .yamlParsing(message): return "Cannot parse YAML file: \(message)" case let .baselineNotReadable(path): - return "Cannot open or read the baseline file at path '\(path)'." + return "Cannot open or read the baseline file at path '\(path.relativeFilepath)'." } } } diff --git a/Source/SwiftLintCore/Models/Location.swift b/Source/SwiftLintCore/Models/Location.swift index 4bb174207c..76d4c22d58 100644 --- a/Source/SwiftLintCore/Models/Location.swift +++ b/Source/SwiftLintCore/Models/Location.swift @@ -2,10 +2,10 @@ import Foundation import SourceKittenFramework import SwiftSyntax -/// The placement of a segment of Swift in a collection of source files. +/// A specific location within a source file. public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { /// The file path on disk for this location. - public let file: String? + public let file: URL? /// The line offset in the file for this location. 1-indexed. public let line: Int? /// The character offset in the file for this location. 1-indexed. @@ -15,23 +15,18 @@ public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { public var description: String { // Xcode likes warnings and errors in the following format: // {full_path_to_file}{:line}{:character}: {error,warning}: {content} - let fileString = file ?? "" + let fileString = file?.path ?? "" let lineString = ":\(line ?? 1)" let charString = ":\(character ?? 1)" return [fileString, lineString, charString].joined() } - /// The file path for this location relative to the current working directory. - public var relativeFile: String? { - file?.replacingOccurrences(of: FileManager.default.currentDirectoryPath + "/", with: "") - } - /// Creates a `Location` by specifying its properties directly. /// /// - parameter file: The file path on disk for this location. /// - parameter line: The line offset in the file for this location. 1-indexed. /// - parameter character: The character offset in the file for this location. 1-indexed. - public init(file: String?, line: Int? = nil, character: Int? = nil) { + public init(file: URL?, line: Int? = nil, character: Int? = nil) { self.file = file self.line = line self.character = character @@ -43,14 +38,12 @@ public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { /// - parameter file: The file for this location. /// - parameter offset: The offset in bytes into the file for this location. public init(file: SwiftLintFile, byteOffset offset: ByteCount) { - self.file = file.path - if let lineAndCharacter = file.stringView.lineAndCharacter(forByteOffset: offset) { - line = lineAndCharacter.line - character = lineAndCharacter.character - } else { - line = nil - character = nil - } + let lineAndCharacter = file.stringView.lineAndCharacter(forByteOffset: offset) + self.init( + file: file.path, + line: lineAndCharacter?.line, + character: lineAndCharacter?.character + ) } /// Creates a `Location` based on a `SwiftLintFile` and a SwiftSyntax `AbsolutePosition` into the file. @@ -68,21 +61,19 @@ public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { /// - parameter file: The file for this location. /// - parameter offset: The offset in UTF8 fragments into the file for this location. public init(file: SwiftLintFile, characterOffset offset: Int) { - self.file = file.path - if let lineAndCharacter = file.stringView.lineAndCharacter(forCharacterOffset: offset) { - line = lineAndCharacter.line - character = lineAndCharacter.character - } else { - line = nil - character = nil - } + let lineAndCharacter = file.stringView.lineAndCharacter(forCharacterOffset: offset) + self.init( + file: file.path, + line: lineAndCharacter?.line, + character: lineAndCharacter?.character + ) } // MARK: Comparable public static func < (lhs: Self, rhs: Self) -> Bool { if lhs.file != rhs.file { - return lhs.file < rhs.file + return lhs.file?.path < rhs.file?.path } if lhs.line != rhs.line { return lhs.line < rhs.line diff --git a/Source/SwiftLintCore/Models/SwiftLintFile.swift b/Source/SwiftLintCore/Models/SwiftLintFile.swift index 4809ed6efc..279813457b 100644 --- a/Source/SwiftLintCore/Models/SwiftLintFile.swift +++ b/Source/SwiftLintCore/Models/SwiftLintFile.swift @@ -3,6 +3,8 @@ import Foundation /// A unit of Swift source code, either on disk or in memory. public final class SwiftLintFile: Sendable { + /// The file's path on disk, if it exists. + public let path: URL? /// The underlying SourceKitten file. public let file: File /// The associated unique identifier for this file. @@ -17,8 +19,9 @@ public final class SwiftLintFile: Sendable { /// - parameter file: A file from SourceKitten. /// - parameter isTestFile: Mark the file as being generated for testing purposes only. /// - parameter isVirtual: Mark the file as virtual (in-memory). - public init(file: File, isTestFile: Bool = false, isVirtual: Bool = false) { + private init(file: File, path: URL? = nil, isTestFile: Bool = false, isVirtual: Bool = false) { self.file = file + self.path = path self.id = UUID() self.isTestFile = isTestFile self.isVirtual = isVirtual @@ -29,17 +32,17 @@ public final class SwiftLintFile: Sendable { /// /// - parameter path: The path to a file on disk. Relative and absolute paths supported. /// - parameter isTestFile: Mark the file as being generated for testing purposes only. - public convenience init?(path: String, isTestFile: Bool = false) { - guard let file = File(path: path) else { return nil } - self.init(file: file, isTestFile: isTestFile) + public convenience init?(path: URL, isTestFile: Bool = false) { + guard let file = File(path: path.filepath) else { return nil } + self.init(file: file, path: path, isTestFile: isTestFile) } /// Creates a `SwiftLintFile` by specifying its path on disk. Unlike the `SwiftLintFile(path:)` initializer, this /// one does not read its contents immediately, but rather traps at runtime when attempting to access its contents. /// /// - parameter path: The path to a file on disk. Relative and absolute paths supported. - public convenience init(pathDeferringReading path: String) { - self.init(file: File(pathDeferringReading: path)) + public convenience init(pathDeferringReading path: URL) { + self.init(file: File(pathDeferringReading: path.filepath), path: path) } /// Creates a `SwiftLintFile` that is not backed by a file on disk by specifying its contents. @@ -50,11 +53,6 @@ public final class SwiftLintFile: Sendable { self.init(file: File(contents: contents), isTestFile: isTestFile, isVirtual: true) } - /// The path on disk for this file. - public var path: String? { - file.path - } - /// The file's contents. public var contents: String { file.contents diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index 37170a9f7f..fed51f0c0c 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -125,10 +125,11 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, hasher.combine(executionMode) } - package func shouldValidate(filePath: String) -> Bool { - let pathRange = filePath.fullNSRange + package func shouldValidate(filePath: URL) -> Bool { + let path = filePath.path + let pathRange = path.fullNSRange let isIncluded = included.isEmpty || included.contains { regex in - regex.regex.firstMatch(in: filePath, range: pathRange) != nil + regex.regex.firstMatch(in: path, range: pathRange) != nil } guard isIncluded else { @@ -136,7 +137,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, } return excluded.allSatisfy { regex in - regex.regex.firstMatch(in: filePath, range: pathRange) == nil + regex.regex.firstMatch(in: path, range: pathRange) == nil } } diff --git a/Source/SwiftLintFramework/Benchmark.swift b/Source/SwiftLintFramework/Benchmark.swift index 76b113155c..c7f54203fb 100644 --- a/Source/SwiftLintFramework/Benchmark.swift +++ b/Source/SwiftLintFramework/Benchmark.swift @@ -19,7 +19,7 @@ struct Benchmark { } mutating func record(file: SwiftLintFile, from start: Date) { - record(id: file.path ?? "", time: -start.timeIntervalSinceNow) + record(id: file.path?.filepath ?? "", time: -start.timeIntervalSinceNow) } func save() { @@ -33,7 +33,7 @@ struct Benchmark { "\(numberFormatter.string(from: NSNumber(value: time))!): \(id)" } let string: String = lines.joined(separator: "\n") + "\n" - let url = URL(fileURLWithPath: "benchmark_\(name)_\(timestamp).txt", isDirectory: false) + let url = URL(filePath: "benchmark_\(name)_\(timestamp).txt", directoryHint: .notDirectory) try? string.data(using: .utf8)?.write(to: url, options: [.atomic]) } } diff --git a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift index f33da18fcd..4689048d9c 100644 --- a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift +++ b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift @@ -64,7 +64,7 @@ extension Array where Element == String { return [arg] } let responseFile = String(arg.dropFirst()) - return (try? String(contentsOf: URL(fileURLWithPath: responseFile, isDirectory: false))).flatMap { + return (try? String(contentsOf: URL(filePath: responseFile, directoryHint: .notDirectory))).flatMap { $0.trimmingCharacters(in: .newlines) .components(separatedBy: "\n") .expandingResponseFiles diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 4776964231..85fae90371 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -20,8 +20,9 @@ private func readFilesFromScriptInputFiles() throws(SwiftLintError) -> [SwiftLin guard let path = environment[variable] else { throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)") } - if path.bridge().isSwiftFile() { - return SwiftLintFile(pathDeferringReading: path) + let pathURL = URL(filePath: path) + if pathURL.isSwiftFile { + return SwiftLintFile(pathDeferringReading: pathURL) } return nil } catch { @@ -41,14 +42,15 @@ private func readFilesFromScriptInputFileLists() throws(SwiftLintError) -> [Swif guard let path = environment[variable] else { throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)") } - if path.bridge().pathExtension == "xcfilelist" { - guard let fileContents = FileManager.default.contents(atPath: path), - let textContents = String(data: fileContents, encoding: .utf8) else { + let pathURL = URL(filePath: path) + if pathURL.pathExtension == "xcfilelist" { + guard let textContents = try? String(contentsOf: pathURL, encoding: .utf8) else { throw SwiftLintError.usageError(description: "Could not read file list at: \(path)") } textContents.enumerateLines { line, _ in - if line.isSwiftFile() { - filesToLint.append(SwiftLintFile(pathDeferringReading: line)) + let lineURL = URL(filePath: line) + if lineURL.isSwiftFile { + filesToLint.append(SwiftLintFile(pathDeferringReading: lineURL)) } } } @@ -107,19 +109,22 @@ extension Configuration { -> [Configuration: [SwiftLintFile]] { if files.isEmpty, !visitor.allowZeroLintableFiles { throw .usageError( - description: "No lintable files found at paths: '\(visitor.options.paths.joined(separator: ", "))'" + description: """ + No lintable files found at paths: \ + '\(visitor.options.paths.map(\.relativePath).joined(separator: ", "))' + """ ) } return files.parallelFilterGroup { file in let fileConfiguration = configuration(for: file) - let fileConfigurationRootPath = fileConfiguration.rootDirectory.bridge() + let fileConfigurationRootPath = fileConfiguration.rootDirectory // Files whose configuration specifies they should be excluded will be skipped let shouldSkip = fileConfiguration.excludedPaths.contains { excludedRelativePath in - let excludedPath = fileConfigurationRootPath.appendingPathComponent(excludedRelativePath) - let filePathComponents = file.path?.bridge().pathComponents ?? [] - let excludedPathComponents = excludedPath.bridge().pathComponents + let excludedPath = fileConfigurationRootPath.appending(path: excludedRelativePath.relativePath) + let filePathComponents = file.path?.pathComponents ?? [] + let excludedPathComponents = excludedPath.pathComponents return filePathComponents.starts(with: excludedPathComponents) } @@ -127,18 +132,18 @@ extension Configuration { } } - private func outputFilename(for path: String, duplicateFileNames: Set) -> String { - let basename = path.bridge().lastPathComponent + private func outputFilename(for path: URL, duplicateFileNames: Set) -> String { + let basename = path.lastPathComponent if !duplicateFileNames.contains(basename) { return basename } - var pathComponents = path.bridge().pathComponents - for component in rootDirectory.bridge().pathComponents where pathComponents.first == component { + var pathComponents = path.pathComponents + for component in rootDirectory.pathComponents where pathComponents.first == component { pathComponents.removeFirst() } - return pathComponents.reduce(URL(fileURLWithPath: "/")) { $0.appendingPathComponent($1) }.filepath + return pathComponents.reduce(URL(filePath: "/")) { $0.appending(path: $1) }.filepath } private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]], @@ -226,7 +231,7 @@ extension Configuration { } } - await Signposts.record(name: "Configuration.Visit", span: .file(linter.file.path ?? "")) { + await Signposts.record(name: "Configuration.Visit", span: .file(linter.file.path?.filepath ?? "")) { await visitor.block(linter) } return linter.file @@ -258,10 +263,10 @@ extension Configuration { } if !options.quiet { let filesInfo: String - if options.paths.isEmpty || options.paths == [""] { + if options.paths.isEmpty || options.paths == [URL.cwd] { filesInfo = "in current working directory" } else { - filesInfo = "at paths \(options.paths.joined(separator: ", "))" + filesInfo = "at paths \(options.paths.map(\.relativePath).joined(separator: ", "))" } queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") @@ -305,7 +310,7 @@ private struct DuplicateCollector { private extension Collection where Element == Linter { var duplicateFileNames: Set { let collector = reduce(into: DuplicateCollector()) { result, linter in - if let filename = linter.file.path?.bridge().lastPathComponent { + if let filename = linter.file.path?.lastPathComponent { if result.all.contains(filename) { result.duplicates.insert(filename) } diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift b/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift index a8294914c7..8517df1331 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Cache.swift @@ -62,7 +62,7 @@ extension Configuration { let cacheRulesDescriptions = rules .map { rule in [type(of: rule).identifier, rule.cacheDescription] } .sorted { $0[0] < $1[0] } - let jsonObject: [Any] = [rootDirectory, cacheRulesDescriptions] + let jsonObject: [Any] = [rootDirectory.filepath, cacheRulesDescriptions] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject) { return jsonData.sha256().toHexString() } @@ -72,20 +72,20 @@ extension Configuration { internal var cacheURL: URL { let baseURL: URL if let path = cachePath { - baseURL = URL(fileURLWithPath: path, isDirectory: true) + baseURL = URL(filePath: path, directoryHint: .isDirectory) } else { #if os(Linux) - baseURL = URL(fileURLWithPath: "/var/tmp/", isDirectory: true) + baseURL = URL(filePath: "/var/tmp/", directoryHint: .isDirectory) #else baseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] #endif } var folder = baseURL - .appendingPathComponent("SwiftLint") - .appendingPathComponent(Version.current.value) + .appending(path: "SwiftLint", directoryHint: .isDirectory) + .appending(path: Version.current.value, directoryHint: .isDirectory) if let buildID = ExecutableInfo.buildID { - folder = folder.appendingPathComponent(buildID) + folder = folder.appending(path: buildID, directoryHint: .isDirectory) } do { diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift index e15d0f5fa6..3cd579d93e 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift @@ -6,7 +6,7 @@ package extension Configuration { private static let defaultRemoteConfigTimeout: TimeInterval = 2 private static let defaultRemoteConfigTimeoutIfCached: TimeInterval = 1 - internal let rootDirectory: String + internal let rootDirectory: URL private let ignoreParentAndChildConfigs: Bool @@ -16,9 +16,9 @@ package extension Configuration { private var isBuilt = false // MARK: - Initializers - internal init(commandLineChildConfigs: [String], rootDirectory: String, ignoreParentAndChildConfigs: Bool) { + internal init(commandLineChildConfigs: [URL], rootDirectory: URL, ignoreParentAndChildConfigs: Bool) { let verticesArray = commandLineChildConfigs.map { config in - Vertex(string: config, rootDirectory: rootDirectory, isInitialVertex: true) + Vertex(string: config.filepath, rootDirectory: rootDirectory.filepath, isInitialVertex: true) } vertices = Set(verticesArray) edges = Set(zip(verticesArray, verticesArray.dropFirst()).map { Edge(parent: $0.0, child: $0.1) }) @@ -28,7 +28,7 @@ package extension Configuration { } /// Dummy init to get a FileGraph that just represents a root directory - internal init(rootDirectory: String) { + internal init(rootDirectory: URL) { self.init( commandLineChildConfigs: [], rootDirectory: rootDirectory, @@ -57,12 +57,12 @@ package extension Configuration { ) } - internal func includesFile(atPath path: String) -> Bool { + internal func includesFile(atPath path: URL) -> Bool { guard isBuilt else { return false } return vertices.contains { vertex in if case let .existing(filePath) = vertex.filePath { - return path == filePath + return path.filepath == filePath } return false @@ -137,12 +137,16 @@ package extension Configuration { remoteConfigTimeoutOverride: TimeInterval?, remoteConfigTimeoutIfCachedOverride: TimeInterval? ) throws { - let key = type == .childConfig ? Configuration.Key.childConfig.rawValue + let key = type == .childConfig + ? Configuration.Key.childConfig.rawValue : Configuration.Key.parentConfig.rawValue if let reference = vertex.configurationDict[key] as? String { - let referencedVertex = Vertex(string: reference, rootDirectory: vertex.rootDirectory, - isInitialVertex: false) + let referencedVertex = Vertex( + string: reference, + rootDirectory: vertex.rootDirectory, + isInitialVertex: false + ) // Local vertices are allowed to have local / remote references // Remote vertices are only allowed to have remote references @@ -150,7 +154,7 @@ package extension Configuration { throw Issue.genericWarning("Remote configs are not allowed to reference local configs.") } let existingVertex = findPossiblyExistingVertex(sameAs: referencedVertex) - let existingVertexCopy = existingVertex.map { $0.copy(withNewRootDirectory: rootDirectory) } + let existingVertexCopy = existingVertex.map { $0.copy(withNewRootDirectory: rootDirectory.filepath) } edges.insert( type == .childConfig @@ -189,7 +193,7 @@ package extension Configuration { // MARK: Validating /// Validates the Graph and throws failures /// If successful, returns array of configuration dicts that represents the graph - private func validate() throws -> [(configurationDict: [String: Any], rootDirectory: String)] { + private func validate() throws -> [(configurationDict: [String: Any], rootDirectory: URL)] { // Detect cycles via back-edge detection during DFS func walkDown(stack: [Vertex]) throws { // Please note that the equality check (`==`), not the identity check (`===`) is used @@ -241,25 +245,26 @@ package extension Configuration { return verticesToMerge.map { ( configurationDict: $0.configurationDict, - rootDirectory: $0.rootDirectory + rootDirectory: $0.rootDirectory.url(directoryHint: .isDirectory) ) } } // MARK: Merging private func merged( - configurationData: [(configurationDict: [String: Any], rootDirectory: String)], + configurationData: [(configurationDict: [String: Any], rootDirectory: URL)], enableAllRules: Bool, onlyRule: [String], cachePath: String? ) throws -> Configuration { // Split into first & remainder; use empty dict for first if the array is empty - let firstConfigurationData = configurationData.first ?? (configurationDict: [:], rootDirectory: "") + let firstConfigurationData = configurationData.first ?? (configurationDict: [:], rootDirectory: URL.cwd) let configurationData = Array(configurationData.dropFirst()) // Build first configuration var firstConfiguration = try Configuration( dict: firstConfigurationData.configurationDict, + location: firstConfigurationData.rootDirectory, enableAllRules: enableAllRules, onlyRule: onlyRule, cachePath: cachePath @@ -269,16 +274,14 @@ package extension Configuration { // firstConfigurationData.rootDirectory may be different from rootDirectory, // e. g. when ../file.yml is passed as the first config firstConfiguration.fileGraph = Self(rootDirectory: rootDirectory) - firstConfiguration.makeIncludedAndExcludedPaths( - relativeTo: rootDirectory, - previousBasePath: firstConfigurationData.rootDirectory - ) + firstConfiguration.makeIncludedAndExcludedPaths(relativeTo: rootDirectory) // Build succeeding configurations return try configurationData.reduce(firstConfiguration) { var childConfiguration = try Configuration( parentConfiguration: $0, dict: $1.configurationDict, + location: $1.rootDirectory, enableAllRules: enableAllRules, onlyRule: onlyRule, cachePath: cachePath diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift index 997680eb94..1058e9eafa 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift @@ -34,7 +34,10 @@ internal extension Configuration.FileGraph { } else { originalRemoteString = nil filePath = .existing( - path: string.bridge().absolutePathRepresentation(rootDirectory: rootDirectory) + path: string.url( + relativeTo: rootDirectory.url(directoryHint: .isDirectory), + directoryHint: .notDirectory + ).filepath ) } self.isInitialVertex = isInitialVertex @@ -74,8 +77,8 @@ internal extension Configuration.FileGraph { private func read(at path: String) throws -> String { guard !path.isEmpty, FileManager.default.fileExists(atPath: path) else { throw isInitialVertex - ? Issue.initialFileNotFound(path: path) - : Issue.fileNotFound(path: path) + ? Issue.initialFileNotFound(path: path.url()) + : Issue.fileNotFound(path: path.url()) } return try String(contentsOfFile: path, encoding: .utf8) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index 0a846fd6e6..d7ffebf6e3 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -13,13 +13,11 @@ extension Configuration { /// - parameter excludeByPrefix: Whether or not it uses the exclude-by-prefix algorithm. /// /// - returns: Files to lint. - public func lintableFiles(inPath path: String, + public func lintableFiles(inPath path: URL, forceExclude: Bool, excludeByPrefix: Bool) -> [SwiftLintFile] { lintablePaths(inPath: path, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix) - .parallelCompactMap { - SwiftLintFile(pathDeferringReading: $0) - } + .parallelCompactMap { SwiftLintFile(pathDeferringReading: $0) } } /// Returns the paths for files that can be linted by SwiftLint in the specified parent path. @@ -33,18 +31,17 @@ extension Configuration { /// /// - returns: Paths for files to lint. func lintablePaths( - inPath path: String, + inPath path: URL, forceExclude: Bool, excludeByPrefix: Bool, fileManager: some LintableFileManager = FileManager.default - ) -> [String] { + ) -> [URL] { let excluder = createExcluder(excludeByPrefix: excludeByPrefix) // Handle single file path. - if path.isFile { + if path.isSwiftFile { return fileManager.filesToLint( inPath: path, - rootDirectory: rootDirectory, excluder: forceExclude ? excluder : .noExclusion ) } @@ -53,35 +50,35 @@ extension Configuration { if includedPaths.isEmpty { return fileManager.filesToLint( inPath: path, - rootDirectory: rootDirectory, excluder: excluder ) } // With included paths, only lint them (after resolving globs). - let pathsToLint = includedPaths.flatMap(Glob.resolveGlob).parallelFlatMap { - fileManager.filesToLint( - inPath: $0, - rootDirectory: rootDirectory, - excluder: excluder - ) - } + let pathsToLint = includedPaths + .flatMap { Glob.resolveGlob($0) } + .parallelFlatMap { + fileManager.filesToLint( + inPath: $0, + excluder: excluder + ) + } // Duplicates may arise, so make them unique. return makeUnique(paths: pathsToLint) } - private func makeUnique(paths: [String]) -> [String] { - #if os(Linux) + private func makeUnique(paths: [URL]) -> [URL] { + #if os(macOS) + let result = NSOrderedSet(array: paths) + #else let result = NSMutableOrderedSet(capacity: paths.count) result.addObjects(from: paths) - #else - let result = NSOrderedSet(array: paths) #endif - return result.array as! [String] // swiftlint:disable:this force_cast + return result.array as! [URL] // swiftlint:disable:this force_cast } - func filteredPaths(in paths: [String], excludeByPrefix: Bool) -> [String] { + func filteredPaths(in paths: [URL], excludeByPrefix: Bool) -> [URL] { let excluder = createExcluder(excludeByPrefix: excludeByPrefix) return paths.filter { !excluder.excludes(path: $0) } } @@ -94,12 +91,12 @@ extension Configuration { return .byPrefix( prefixes: excludedPaths .flatMap { Glob.resolveGlob($0) } - .map { $0.absolutePathStandardized() } + .map(\.path) ) } return .matching( matchers: excludedPaths.flatMap { - Glob.createFilenameMatchers(root: rootDirectory, pattern: $0) + Glob.createFilenameMatchers(root: rootDirectory.path, pattern: $0.path) } ) } diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Merging.swift b/Source/SwiftLintFramework/Configuration/Configuration+Merging.swift index 1546e3dbdf..f85bb28253 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Merging.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Merging.swift @@ -8,12 +8,9 @@ extension Configuration { // MARK: - Methods: Merging package func merged( withChild childConfiguration: Configuration, - rootDirectory: String = "" + rootDirectory: URL = URL.cwd ) -> Configuration { - let mergedIncludedAndExcluded = mergedIncludedAndExcluded( - with: childConfiguration, - rootDirectory: rootDirectory - ) + let mergedIncludedAndExcluded = mergedIncludedAndExcluded(with: childConfiguration) return Configuration( rulesWrapper: rulesWrapper.merged(with: childConfiguration.rulesWrapper), @@ -34,34 +31,22 @@ extension Configuration { } private func mergedIncludedAndExcluded( - with childConfiguration: Configuration, - rootDirectory: String - ) -> (includedPaths: [String], excludedPaths: [String]) { - // Render paths relative to their respective root paths → makes them comparable - let childConfigIncluded = childConfiguration.includedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: childConfiguration.rootDirectory) - } - - let childConfigExcluded = childConfiguration.excludedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: childConfiguration.rootDirectory) - } + with childConfiguration: Configuration + ) -> (includedPaths: [URL], excludedPaths: [URL]) { + let childConfigIncluded = childConfiguration.includedPaths + let childConfigExcluded = childConfiguration.excludedPaths - let parentConfigIncluded = includedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: self.rootDirectory) + // Prefer child configuration over parent configuration + let includedPaths = includedPaths.filter { includePath in + !childConfigExcluded.contains(includePath) } - - let parentConfigExcluded = excludedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: self.rootDirectory) + let excludedPaths = excludedPaths.filter { excludePath in + !childConfigIncluded.contains(excludePath) } - // Prefer child configuration over parent configuration - let includedPaths = parentConfigIncluded.filter { !childConfigExcluded.contains($0) } + childConfigIncluded - let excludedPaths = parentConfigExcluded.filter { !childConfigIncluded.contains($0) } + childConfigExcluded - - // Return paths relative to the provided root directory return ( - includedPaths: includedPaths.map { $0.path(relativeTo: rootDirectory) }, - excludedPaths: excludedPaths.map { $0.path(relativeTo: rootDirectory) } + includedPaths: includedPaths + childConfigIncluded, + excludedPaths: excludedPaths + childConfigExcluded ) } @@ -85,16 +70,15 @@ extension Configuration { /// /// - returns: A new configuration. public func configuration(for file: SwiftLintFile) -> Configuration { - (file.path?.bridge().deletingLastPathComponent).map(configuration(forDirectory:)) ?? self + (file.path?.deletingLastPathComponent()).map(configuration(forDirectory:)) ?? self } - private func configuration(forDirectory directory: String) -> Configuration { + private func configuration(forDirectory directory: URL) -> Configuration { // If the configuration was explicitly specified via the `--config` param, don't use nested configs guard !basedOnCustomConfigurationFiles else { return self } - let directoryNSString = directory.bridge() - let configurationSearchPath = directoryNSString.appendingPathComponent(Self.defaultFileName) - let cacheIdentifier = "nestedPath" + rootDirectory + configurationSearchPath + let configurationSearchPath = directory.appending(path: Self.defaultFileName, directoryHint: .notDirectory) + let cacheIdentifier = "nestedPath" + rootDirectory.path + configurationSearchPath.path if Self.getIsNestedConfigurationSelf(forIdentifier: cacheIdentifier) == true { return self @@ -107,9 +91,7 @@ extension Configuration { if directory == rootDirectory { // Use self if at level self config = self - } else if - FileManager.default.fileExists(atPath: configurationSearchPath), - !fileGraph.includesFile(atPath: configurationSearchPath) { + } else if configurationSearchPath.exists, !fileGraph.includesFile(atPath: configurationSearchPath) { // Use self merged with the nested config that was found // iff that nested config has not already been used to build the main config @@ -123,9 +105,9 @@ extension Configuration { // Cache merged result to circumvent heavy merge recomputations config.setCached(forIdentifier: cacheIdentifier) - } else if directory != "/" { + } else if directory.path != "/" { // If we are not at the root path, continue down the tree - config = configuration(forDirectory: directoryNSString.deletingLastPathComponent) + config = configuration(forDirectory: directory.deletingLastPathComponent()) } else { // Fallback to self config = self diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index bf327accdb..12251a5f50 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -1,3 +1,5 @@ +import Foundation + extension Configuration { // MARK: - Subtypes internal enum Key: String, CaseIterable { @@ -34,6 +36,7 @@ extension Configuration { /// - parameter parentConfiguration: The parent configuration, if any. /// - parameter dict: The untyped dictionary to serve as the input for this typed configuration. /// Typically generated from a YAML-formatted file. + /// - parameter location: The location of the configuration file. /// - parameter ruleList: The list of rules to be available to this configuration. /// - parameter enableAllRules: Whether all rules from `ruleList` should be enabled, regardless of the /// settings in `dict`. @@ -41,6 +44,7 @@ extension Configuration { public init( parentConfiguration: Configuration? = nil, dict: [String: Any], + location: URL = URL.cwd, ruleList: RuleList = RuleRegistry.shared.list, enableAllRules: Bool = false, onlyRule: [String] = [], @@ -96,8 +100,8 @@ extension Configuration { rulesMode: rulesMode, allRulesWrapped: allRulesWrapped, ruleList: ruleList, - includedPaths: defaultStringArray(dict[Key.included.rawValue]), - excludedPaths: defaultStringArray(dict[Key.excluded.rawValue]), + includedPaths: defaultStringArray(dict[Key.included.rawValue]).map { $0.url(relativeTo: location) }, + excludedPaths: defaultStringArray(dict[Key.excluded.rawValue]).map { $0.url(relativeTo: location) }, indentation: Self.getIndentationLogIfInvalid(from: dict), warningThreshold: dict[Key.warningThreshold.rawValue] as? Int, reporter: dict[Key.reporter.rawValue] as? String ?? XcodeReporter.identifier, @@ -106,8 +110,10 @@ extension Configuration { allowZeroLintableFiles: dict[Key.allowZeroLintableFiles.rawValue] as? Bool ?? false, strict: dict[Key.strict.rawValue] as? Bool ?? false, lenient: dict[Key.lenient.rawValue] as? Bool ?? false, - baseline: dict[Key.baseline.rawValue] as? String, - writeBaseline: dict[Key.writeBaseline.rawValue] as? String, + baseline: (dict[Key.baseline.rawValue] as? String)? + .url(relativeTo: location, directoryHint: .notDirectory), + writeBaseline: (dict[Key.writeBaseline.rawValue] as? String)? + .url(relativeTo: location, directoryHint: .notDirectory), checkForUpdates: dict[Key.checkForUpdates.rawValue] as? Bool ?? false ) } diff --git a/Source/SwiftLintFramework/Configuration/Configuration.swift b/Source/SwiftLintFramework/Configuration/Configuration.swift index ee3e2fa788..a13e492e79 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration.swift @@ -15,10 +15,10 @@ public struct Configuration { // MARK: Public Instance /// The paths that should be included when linting - public private(set) var includedPaths: [String] + public private(set) var includedPaths: [URL] /// The paths that should be excluded when linting - public private(set) var excludedPaths: [String] + public private(set) var excludedPaths: [URL] /// The style to use when indenting Swift source code. public let indentation: IndentationStyle @@ -42,10 +42,10 @@ public struct Configuration { public let lenient: Bool /// The path to read a baseline from. - public let baseline: String? + public let baseline: URL? /// The path to write a baseline to. - public let writeBaseline: String? + public let writeBaseline: URL? /// Check for updates. public let checkForUpdates: Bool @@ -63,7 +63,7 @@ public struct Configuration { /// By default, the root directory is the current working directory, /// but during some merging algorithms it may be used differently. /// The rootDirectory also serves as the stopping point when searching for nested configs along the file hierarchy. - public var rootDirectory: String { fileGraph.rootDirectory } + public var rootDirectory: URL { fileGraph.rootDirectory } /// The rules mode used for this configuration. public var rulesMode: RulesMode { rulesWrapper.mode } @@ -78,8 +78,8 @@ public struct Configuration { internal init( rulesWrapper: RulesWrapper, fileGraph: FileGraph, - includedPaths: [String], - excludedPaths: [String], + includedPaths: [URL], + excludedPaths: [URL], indentation: IndentationStyle, warningThreshold: Int?, reporter: String?, @@ -87,8 +87,8 @@ public struct Configuration { allowZeroLintableFiles: Bool, strict: Bool, lenient: Bool, - baseline: String?, - writeBaseline: String?, + baseline: URL?, + writeBaseline: URL?, checkForUpdates: Bool ) { self.rulesWrapper = rulesWrapper @@ -158,8 +158,8 @@ public struct Configuration { allRulesWrapped: [ConfigurationRuleWrapper]? = nil, ruleList: RuleList = RuleRegistry.shared.list, fileGraph: FileGraph? = nil, - includedPaths: [String] = [], - excludedPaths: [String] = [], + includedPaths: [URL] = [], + excludedPaths: [URL] = [], indentation: IndentationStyle = .default, warningThreshold: Int? = nil, reporter: String? = nil, @@ -168,8 +168,8 @@ public struct Configuration { allowZeroLintableFiles: Bool = false, strict: Bool = false, lenient: Bool = false, - baseline: String? = nil, - writeBaseline: String? = nil, + baseline: URL? = nil, + writeBaseline: URL? = nil, checkForUpdates: Bool = false ) { if let pinnedVersion, pinnedVersion != Version.current.value { @@ -186,9 +186,7 @@ public struct Configuration { allRulesWrapped: allRulesWrapped ?? (try? ruleList.allRulesWrapped()) ?? [], aliasResolver: { ruleList.identifier(for: $0) ?? $0 } ), - fileGraph: fileGraph ?? FileGraph( - rootDirectory: FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized() - ), + fileGraph: fileGraph ?? FileGraph(rootDirectory: URL.cwd), includedPaths: includedPaths, excludedPaths: excludedPaths, indentation: indentation, @@ -217,7 +215,7 @@ public struct Configuration { /// - parameter useDefaultConfigOnFailure: If this value is specified, it will override the normal behavior. /// This is only intended for tests checking whether invalid configs fail. public init( - configurationFiles: [String], // No default value here to avoid ambiguous Configuration() initializer + configurationFiles: [URL], // No default value here to avoid ambiguous Configuration() initializer enableAllRules: Bool = false, onlyRule: [String] = [], cachePath: String? = nil, @@ -235,10 +233,12 @@ public struct Configuration { // Store whether there are custom configuration files; use default config file name if there are none let hasCustomConfigurationFiles: Bool = configurationFiles.isNotEmpty - let configurationFiles = configurationFiles.isEmpty ? [Self.defaultFileName] : configurationFiles + let configurationFiles = configurationFiles.isEmpty + ? [Self.defaultFileName.url()] + : configurationFiles defer { basedOnCustomConfigurationFiles = hasCustomConfigurationFiles } - let currentWorkingDirectory = FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized() + let currentWorkingDirectory = URL.cwd let rulesMode: RulesMode = if enableAllRules { .allCommandLine } else if onlyRule.isNotEmpty { @@ -292,14 +292,9 @@ public struct Configuration { } // MARK: - Methods: Internal - mutating func makeIncludedAndExcludedPaths(relativeTo newBasePath: String, previousBasePath: String) { - includedPaths = includedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: previousBasePath).path(relativeTo: newBasePath) - } - - excludedPaths = excludedPaths.map { - $0.bridge().absolutePathRepresentation(rootDirectory: previousBasePath).path(relativeTo: newBasePath) - } + mutating func makeIncludedAndExcludedPaths(relativeTo newBasePath: URL) { + includedPaths = includedPaths.map { $0.relative(to: newBasePath) } + excludedPaths = excludedPaths.map { $0.relative(to: newBasePath) } } } diff --git a/Source/SwiftLintFramework/Documentation/RuleListDocumentation.swift b/Source/SwiftLintFramework/Documentation/RuleListDocumentation.swift index eef3a75af7..ef55d63cdb 100644 --- a/Source/SwiftLintFramework/Documentation/RuleListDocumentation.swift +++ b/Source/SwiftLintFramework/Documentation/RuleListDocumentation.swift @@ -21,7 +21,7 @@ public struct RuleListDocumentation { public func write(to url: URL) throws { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) func write(_ text: String, toFile file: String) throws { - try text.write(to: url.appendingPathComponent(file), atomically: false, encoding: .utf8) + try text.write(to: url.appending(path: file), atomically: false, encoding: .utf8) } try write(indexContents, toFile: "Rule Directory.md") try write(swiftSyntaxDashboardContents, toFile: "Swift Syntax Dashboard.md") diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 766e763d0b..492014847f 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -13,7 +13,7 @@ public protocol LintableFileManager { /// - parameter excluder: The excluder used to filter out files that should not be linted. /// /// - returns: Files to lint. - func filesToLint(inPath path: String, rootDirectory: String?, excluder: Excluder) -> [String] + func filesToLint(inPath path: URL, excluder: Excluder) -> [URL] /// Returns the date when the file at the specified path was last modified. Returns `nil` if the file cannot be /// found or its last modification date cannot be determined. @@ -21,7 +21,7 @@ public protocol LintableFileManager { /// - parameter path: The file whose modification date should be determined. /// /// - returns: A date, if one was determined. - func modificationDate(forFileAtPath path: String) -> Date? + func modificationDate(forFileAtPath path: URL) -> Date? } /// An excluder for filtering out files that should not be linted. @@ -33,75 +33,70 @@ public enum Excluder { /// An excluder that does not exclude any files. case noExclusion - func excludes(path: String) -> Bool { - switch self { + func excludes(path: URL) -> Bool { + let standardized = path.standardized.path + return switch self { case let .matching(matchers): - matchers.contains(where: { $0.match(filename: path) }) + matchers.contains(where: { $0.match(filename: standardized) }) case let .byPrefix(prefixes): - prefixes.contains(where: { path.hasPrefix($0) }) + prefixes.contains(where: { standardized.hasPrefix($0) }) case .noExclusion: false } } } -extension FileManager: LintableFileManager, @unchecked @retroactive Sendable { - public func filesToLint(inPath path: String, - rootDirectory: String? = nil, - excluder: Excluder) -> [String] { - let absolutePath = URL( - fileURLWithPath: path.absolutePathRepresentation(rootDirectory: rootDirectory ?? currentDirectoryPath) - ) +#if os(macOS) +extension FileManager: @unchecked @retroactive Sendable {} +#endif +extension FileManager: LintableFileManager { + public func filesToLint(inPath path: URL, excluder: Excluder) -> [URL] { // If path is a file, filter and return it directly. - if absolutePath.isSwiftFile { - let filePath = absolutePath.standardized.filepath - return excluder.excludes(path: filePath) ? [] : [filePath] + if path.isSwiftFile { + return excluder.excludes(path: path) ? [] : [path] } // Fast path when there are no exclusions. if case .noExclusion = excluder { - return subpaths(atPath: absolutePath.filepath)?.parallelCompactMap { element in - let absoluteElementPath = URL(fileURLWithPath: element, relativeTo: absolutePath) - return absoluteElementPath.isSwiftFile ? absoluteElementPath.standardized.filepath : nil + return subpaths(atPath: path.filepath)?.parallelCompactMap { element in + let absoluteElementPath = element.url(relativeTo: path) + return absoluteElementPath.isSwiftFile ? absoluteElementPath : nil } ?? [] } - return collectFiles(atPath: absolutePath, excluder: excluder) + return collectFiles(atPath: path, excluder: excluder) } - private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [String] { - guard let root = absolutePath.filepathGuarded, let enumerator = enumerator(atPath: root) else { + private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [URL] { + guard let enumerator = enumerator(atPath: absolutePath.filepath) else { return [] } - var files = [String]() - var directoriesToWalk = [String]() + var files = [URL]() + var directoriesToWalk = [URL]() while let element = enumerator.nextObject() as? String { - let absoluteElementPath = URL(fileURLWithPath: element, relativeTo: absolutePath) - guard let absoluteStandardizedElementPath = absoluteElementPath.standardized.filepathGuarded else { - continue - } - if absoluteElementPath.path.isFile { + let absoluteElementPath = element.url(relativeTo: absolutePath) + if absoluteElementPath.isFile { if absoluteElementPath.pathExtension == "swift", - !excluder.excludes(path: absoluteStandardizedElementPath) { - files.append(absoluteStandardizedElementPath) + !excluder.excludes(path: absoluteElementPath) { + files.append(absoluteElementPath) } } else { enumerator.skipDescendants() - if !excluder.excludes(path: absoluteStandardizedElementPath) { - directoriesToWalk.append(absoluteStandardizedElementPath) + if !excluder.excludes(path: absoluteElementPath) { + directoriesToWalk.append(absoluteElementPath) } } } return files + directoriesToWalk.parallelFlatMap { - collectFiles(atPath: URL(fileURLWithPath: $0, isDirectory: true), excluder: excluder) + collectFiles(atPath: $0, excluder: excluder) } } - public func modificationDate(forFileAtPath path: String) -> Date? { - (try? attributesOfItem(atPath: path))?[.modificationDate] as? Date + public func modificationDate(forFileAtPath path: URL) -> Date? { + (try? attributesOfItem(atPath: path.filepath))?[.modificationDate] as? Date } } diff --git a/Source/SwiftLintFramework/Helpers/Glob.swift b/Source/SwiftLintFramework/Helpers/Glob.swift index e7cf451432..c41ccc761c 100644 --- a/Source/SwiftLintFramework/Helpers/Glob.swift +++ b/Source/SwiftLintFramework/Helpers/Glob.swift @@ -17,16 +17,16 @@ import WinSDK // Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3 struct Glob { - static func resolveGlob(_ pattern: String) -> [String] { + static func resolveGlob(_ pattern: URL) -> [URL] { let globCharset = CharacterSet(charactersIn: "*?[]") - guard pattern.rangeOfCharacter(from: globCharset) != nil else { + guard pattern.path.rangeOfCharacter(from: globCharset) != nil else { return [pattern] } return expandGlobstar(pattern: pattern) - .reduce(into: [String]()) { paths, pattern in + .reduce(into: [URL]()) { paths, pattern in #if os(Windows) - URL(fileURLWithPath: pattern).withUnsafeFileSystemRepresentation { + pattern.withUnsafeFileSystemRepresentation { var ffd = WIN32_FIND_DATAW() let hDirectory: HANDLE = String(cString: $0!).withCString(encodedAs: UTF16.self) { @@ -43,7 +43,7 @@ struct Glob { } } if path == "." || path == ".." { continue } - paths.append(path) + paths.append(path.url()) } while FindNextFileW(hDirectory, &ffd) } #else @@ -55,19 +55,17 @@ struct Glob { #else let flags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK #endif - if glob(pattern, flags, nil, &globResult) == 0 { + if glob(pattern.path, flags, nil, &globResult) == 0 { paths.append(contentsOf: populateFiles(globResult: globResult)) } #endif } .unique - .sorted() - .map { $0.absolutePathStandardized() } } static func createFilenameMatchers(root: String, pattern: String) -> [FilenameMatcher] { var absolutPathPattern = pattern - if !pattern.starts(with: root) { + if !pattern.starts(with: "/") { // If the root is not already part of the pattern, prepend it. absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern } @@ -90,22 +88,22 @@ struct Glob { // MARK: Private - private static func expandGlobstar(pattern: String) -> [String] { - guard pattern.contains("**") else { + private static func expandGlobstar(pattern: URL) -> [URL] { + guard pattern.path.contains("**") else { return [pattern] } - var parts = pattern.components(separatedBy: "**") + var parts = pattern.filepath.components(separatedBy: "**") let firstPart = parts.removeFirst() let fileManager = FileManager.default guard firstPart.isEmpty || fileManager.fileExists(atPath: firstPart) else { return [] } let searchPath = firstPart.isEmpty ? fileManager.currentDirectoryPath : firstPart - var directories = [String]() + var directories = [URL]() do { directories = try fileManager.subpathsOfDirectory(atPath: searchPath).compactMap { subpath in - let fullPath = firstPart.bridge().appendingPathComponent(subpath) - guard isDirectory(path: fullPath) else { return nil } + let fullPath = firstPart.url().appending(path: subpath) + guard fullPath.isDirectory else { return nil } return fullPath } } catch { @@ -113,45 +111,33 @@ struct Glob { } // Check the base directory for the glob star as well. - directories.insert(firstPart, at: 0) + directories.insert(firstPart.url(), at: 0) var lastPart = parts.joined(separator: "**") - var results = [String]() + var results = [URL]() // Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/" if lastPart.isEmpty { - results.append(firstPart) + results.append(firstPart.url()) lastPart = "*" } for directory in directories { - let partiallyResolvedPattern: String - if directory.isEmpty { - partiallyResolvedPattern = lastPart.starts(with: "/") ? String(lastPart.dropFirst()) : lastPart - } else { - partiallyResolvedPattern = directory.bridge().appendingPathComponent(lastPart) - } - results.append(contentsOf: expandGlobstar(pattern: partiallyResolvedPattern)) + results.append(contentsOf: expandGlobstar(pattern: directory.appending(path: lastPart))) } return results } - private static func isDirectory(path: String) -> Bool { - var isDirectoryBool = ObjCBool(false) - let isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool) - return isDirectory && isDirectoryBool.boolValue - } - #if !os(Windows) - private static func populateFiles(globResult: glob_t) -> [String] { + private static func populateFiles(globResult: glob_t) -> [URL] { #if os(Linux) let matchCount = globResult.gl_pathc #else let matchCount = globResult.gl_matchc #endif return (0.. Arguments { [] } + func arguments(forFile _: URL?) -> Arguments { [] } // MARK: - Private private class ArrayCompilerInvocations: CompilerInvocations { - private let invocationsByArgument: [String: [Arguments]] + private let invocationsByArgument: [URL: [Arguments]] init(invocations: [Arguments]) { // Store invocations by the path, so next when we'll be asked for arguments, // we'll be able to return them faster self.invocationsByArgument = invocations.reduce(into: [:]) { result, arguments in - arguments.forEach { result[$0, default: []].append(arguments) } + arguments.forEach { result[URL(filePath: $0), default: []].append(arguments) } } } - override func arguments(forFile path: String?) -> Arguments { - path.flatMap { path in - invocationsByArgument[path]?.first - } ?? [] + override func arguments(forFile path: URL?) -> Arguments { + if let path, let invocation = invocationsByArgument[path]?.first { + return invocation + } + return [] } } @@ -43,10 +44,10 @@ class CompilerInvocations { self.compileCommands = compileCommands } - override func arguments(forFile path: String?) -> Arguments { + override func arguments(forFile path: URL?) -> Arguments { path.flatMap { path in - compileCommands[path] ?? - compileCommands[path.path(relativeTo: FileManager.default.currentDirectoryPath)] + compileCommands[path.filepath] ?? + compileCommands[path.relativeFilepath] } ?? [] } } @@ -99,7 +100,7 @@ struct LintableFilesVisitor { } } - func shouldSkipFile(atPath path: String?) -> Bool { + func shouldSkipFile(atPath path: URL?) -> Bool { switch mode { case .lint: return false diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index d2a6119e44..d1e46b0e4c 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -410,7 +410,10 @@ public struct CollectedLinter { if file.parserDiagnostics.isNotEmpty { queuedPrintError( - "warning: Skipping correcting file because it produced Swift parser errors: \(file.path ?? "")" + """ + warning: Skipping correcting file '\(file.path?.filepath ?? "")' \ + because it produced Swift parser errors + """ ) queuedPrintError(toJSON(["diagnostics": file.parserDiagnostics])) return [:] diff --git a/Source/SwiftLintFramework/Models/LinterCache.swift b/Source/SwiftLintFramework/Models/LinterCache.swift index c6c541255a..c368566ef6 100644 --- a/Source/SwiftLintFramework/Models/LinterCache.swift +++ b/Source/SwiftLintFramework/Models/LinterCache.swift @@ -57,7 +57,7 @@ public final class LinterCache { self.swiftVersion = swiftVersion } - internal func cache(violations: [StyleViolation], forFile file: String, configuration: Configuration) { + internal func cache(violations: [StyleViolation], forFile file: URL, configuration: Configuration) { guard let lastModification = fileManager.modificationDate(forFileAtPath: file) else { return } @@ -66,15 +66,18 @@ public final class LinterCache { writeCacheLock.lock() var filesCache = writeCache[configurationDescription] ?? .empty - filesCache.entries[file] = FileCacheEntry(violations: violations, lastModification: lastModification, - swiftVersion: swiftVersion) + filesCache.entries[file.filepath] = FileCacheEntry( + violations: violations, + lastModification: lastModification, + swiftVersion: swiftVersion + ) writeCache[configurationDescription] = filesCache writeCacheLock.unlock() } - internal func violations(forFile file: String, configuration: Configuration) -> [StyleViolation]? { + internal func violations(forFile file: URL, configuration: Configuration) -> [StyleViolation]? { guard let lastModification = fileManager.modificationDate(forFileAtPath: file), - let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file], + let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file.filepath], entry.lastModification == lastModification, entry.swiftVersion == swiftVersion else { @@ -109,7 +112,7 @@ public final class LinterCache { let fileCacheEntries = readCache[description]?.entries.merging(writeFileCache.entries) { _, write in write } let fileCache = fileCacheEntries.map(FileCache.init) ?? writeFileCache let data = try encoder.encode(fileCache) - let file = url.appendingPathComponent(description).appendingPathExtension(Self.fileExtension) + let file = url.appending(path: description).appendingPathExtension(Self.fileExtension) try data.write(to: file, options: .atomic) } } @@ -132,7 +135,7 @@ public final class LinterCache { return .empty } - let file = location.appendingPathComponent(cacheDescription).appendingPathExtension(Self.fileExtension) + let file = location.appending(path: cacheDescription).appendingPathExtension(Self.fileExtension) let data = try? Data(contentsOf: file) let fileCache = data.flatMap { try? Decoder().decode(FileCache.self, from: $0) } ?? .empty lazyReadCache[cacheDescription] = fileCache diff --git a/Source/SwiftLintFramework/Reporters/CSVReporter.swift b/Source/SwiftLintFramework/Reporters/CSVReporter.swift index 023f32b68f..d6dbd5acfc 100644 --- a/Source/SwiftLintFramework/Reporters/CSVReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CSVReporter.swift @@ -27,7 +27,7 @@ struct CSVReporter: Reporter { private static func csvRow(for violation: StyleViolation) -> String { [ - violation.location.file?.escapedForCSV() ?? "", + violation.location.file?.path.escapedForCSV() ?? "", violation.location.line?.description ?? "", violation.location.character?.description ?? "", violation.severity.rawValue.capitalized, diff --git a/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift b/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift index 47327648c6..47f9f0a1db 100644 --- a/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift @@ -1,3 +1,5 @@ +import Foundation + /// Reports violations as XML conforming to the Checkstyle specification, as defined here: /// https://www.jetbrains.com/help/teamcity/xml-report-processing.html struct CheckstyleReporter: Reporter { @@ -11,7 +13,7 @@ struct CheckstyleReporter: Reporter { [ "\n", violations - .group(by: { ($0.location.file ?? "").escapedForXML() }) + .group(by: { ($0.location.file?.path ?? "").escapedForXML() }) .sorted(by: { $0.key < $1.key }) .map(generateForViolationFile).joined(), "\n", diff --git a/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift b/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift index 90a622d2c1..1f8bbb7d74 100644 --- a/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift @@ -26,7 +26,7 @@ struct CodeClimateReporter: Reporter { "engine_name": "SwiftLint", "fingerprint": generateFingerprint(violation), "location": [ - "path": violation.location.relativeFile ?? NSNull() as Any, + "path": violation.location.file?.relativeFilepath ?? NSNull() as Any, "lines": [ "begin": violation.location.line ?? NSNull() as Any, "end": violation.location.line ?? NSNull() as Any, @@ -38,14 +38,10 @@ struct CodeClimateReporter: Reporter { } internal static func generateFingerprint(_ violation: StyleViolation) -> String { - let fingerprintLocation = Location( - file: violation.location.relativeFile, - line: violation.location.line, - character: violation.location.character - ) - - return [ - "\(fingerprintLocation)", + [ + "\(violation.location.file?.relativeFilepath ?? "")", + "\(violation.location.line ?? 0)", + "\(violation.location.character ?? 0)", "\(violation.ruleIdentifier)", ].joined().sha256() } diff --git a/Source/SwiftLintFramework/Reporters/EmojiReporter.swift b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift index 64e8559526..3bdda24692 100644 --- a/Source/SwiftLintFramework/Reporters/EmojiReporter.swift +++ b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift @@ -1,3 +1,5 @@ +import Foundation + /// Reports violations in a format that's both fun and easy to read. struct EmojiReporter: Reporter { // MARK: - Reporter Conformance @@ -8,8 +10,8 @@ struct EmojiReporter: Reporter { static func generateReport(_ violations: [StyleViolation]) -> String { violations - .group { $0.location.file ?? "Other" } - .sorted { $0.key < $1.key } + .group { $0.location.file?.path ?? "Other" } + .sorted { $0.key.lowercased() < $1.key.lowercased() } .map(report) .joined(separator: "\n") } diff --git a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift index cbe31039fd..d2c8ed0789 100644 --- a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift +++ b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift @@ -19,7 +19,7 @@ struct GitHubActionsLoggingReporter: Reporter { // ::(warning|error) file={relative_path_to_file},line={:line},col={:character}::{content} [ "::\(violation.severity.rawValue) ", - "file=\(violation.location.relativeFile ?? ""),", + "file=\(violation.location.file?.relativeFilepath ?? ""),", "line=\(violation.location.line ?? 1),", "col=\(violation.location.character ?? 1)::", violation.reason, diff --git a/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift b/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift index 37c1777f85..6aec4ab98d 100644 --- a/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift +++ b/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift @@ -9,7 +9,7 @@ struct GitLabJUnitReporter: Reporter { static func generateReport(_ violations: [StyleViolation]) -> String { "\n" + violations.map({ violation -> String in - let fileName = (violation.location.relativeFile ?? "").escapedForXML() + let fileName = (violation.location.file?.relativeFilepath ?? "").escapedForXML() let line = violation.location.line.map(String.init) let column = violation.location.character.map(String.init) diff --git a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift index 37916a3d4f..9aafce73de 100644 --- a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift +++ b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift @@ -152,7 +152,7 @@ struct HTMLReporter: Reporter { private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String { let severity: String = violation.severity.rawValue.capitalized let location = violation.location - let file: String = (violation.location.relativeFile ?? "").escapedForXML() + let file: String = (violation.location.file?.relativeFilepath ?? "").escapedForXML() let line: Int = location.line ?? 0 let character: Int = location.character ?? 0 return """ diff --git a/Source/SwiftLintFramework/Reporters/JSONReporter.swift b/Source/SwiftLintFramework/Reporters/JSONReporter.swift index 334152c6c7..50158c7120 100644 --- a/Source/SwiftLintFramework/Reporters/JSONReporter.swift +++ b/Source/SwiftLintFramework/Reporters/JSONReporter.swift @@ -17,7 +17,7 @@ struct JSONReporter: Reporter { private static func dictionary(for violation: StyleViolation) -> [String: Any] { [ - "file": violation.location.file ?? NSNull() as Any, + "file": violation.location.file?.path ?? NSNull() as Any, "line": violation.location.line ?? NSNull() as Any, "character": violation.location.character ?? NSNull() as Any, "severity": violation.severity.rawValue.capitalized, diff --git a/Source/SwiftLintFramework/Reporters/JUnitReporter.swift b/Source/SwiftLintFramework/Reporters/JUnitReporter.swift index 0ac1d8f689..5b1ed98072 100644 --- a/Source/SwiftLintFramework/Reporters/JUnitReporter.swift +++ b/Source/SwiftLintFramework/Reporters/JUnitReporter.swift @@ -24,7 +24,7 @@ struct JUnitReporter: Reporter { } private static func testCase(for violation: StyleViolation) -> String { - let fileName = (violation.location.file ?? "").escapedForXML() + let fileName = (violation.location.file?.path ?? "").escapedForXML() let reason = violation.reason.escapedForXML() let severity = violation.severity.rawValue.capitalized let lineNumber = String(violation.location.line ?? 0) diff --git a/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift index 642f30faab..82804124fa 100644 --- a/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift +++ b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift @@ -25,7 +25,7 @@ struct MarkdownReporter: Reporter { private static func markdownRow(for violation: StyleViolation) -> String { [ - violation.location.file?.escapedForMarkdown() ?? "", + violation.location.file?.path.escapedForMarkdown() ?? "", violation.location.line?.description ?? "", severity(for: violation.severity), violation.ruleName.escapedForMarkdown() + ": " + violation.reason.escapedForMarkdown(), diff --git a/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift b/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift index c61586e450..ee69378aba 100644 --- a/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift +++ b/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift @@ -19,7 +19,7 @@ struct RelativePathReporter: Reporter { // {relative_path_to_file}{:line}{:character}: {error,warning}: {content} [ - "\(violation.location.relativeFile ?? "")", + "\(violation.location.file?.relativeFilepath ?? "")", ":\(violation.location.line ?? 1)", ":\(violation.location.character ?? 1): ", "\(violation.severity.rawValue): ", diff --git a/Source/SwiftLintFramework/Reporters/SARIFReporter.swift b/Source/SwiftLintFramework/Reporters/SARIFReporter.swift index b5b516fd09..a94b35f51b 100644 --- a/Source/SwiftLintFramework/Reporters/SARIFReporter.swift +++ b/Source/SwiftLintFramework/Reporters/SARIFReporter.swift @@ -71,7 +71,7 @@ struct SARIFReporter: Reporter { return [ "physicalLocation": [ "artifactLocation": [ - "uri": location.relativeFile ?? "" + "uri": location.file?.relativeFilepath ?? "" ], "region": [ "startLine": line, @@ -84,7 +84,7 @@ struct SARIFReporter: Reporter { return [ "physicalLocation": [ "artifactLocation": [ - "uri": location.file ?? "" + "uri": location.file?.relativeFilepath ?? "" ], ], ] diff --git a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift index 3e014c9e13..b15ec26dd2 100644 --- a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift +++ b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift @@ -21,7 +21,7 @@ struct SonarQubeReporter: Reporter { "ruleId": violation.ruleIdentifier, "primaryLocation": [ "message": violation.reason, - "filePath": violation.location.relativeFile ?? "", + "filePath": violation.location.file?.relativeFilepath ?? "", "textRange": [ "startLine": violation.location.line ?? 1 ] as Any, diff --git a/Source/swiftlint-dev/Reporters+Register.swift b/Source/swiftlint-dev/Reporters+Register.swift index adb181dc3a..4fe6c76b9f 100644 --- a/Source/swiftlint-dev/Reporters+Register.swift +++ b/Source/swiftlint-dev/Reporters+Register.swift @@ -13,10 +13,10 @@ extension SwiftLintDev.Reporters { ) private var reportersDirectory: URL { - URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent("Source", isDirectory: true) - .appendingPathComponent("SwiftLintFramework", isDirectory: true) - .appendingPathComponent("Reporters", isDirectory: true) + URL(filePath: FileManager.default.currentDirectoryPath) + .appending(path: "Source", directoryHint: .isDirectory) + .appending(path: "SwiftLintFramework", directoryHint: .isDirectory) + .appending(path: "Reporters", directoryHint: .isDirectory) } func run() throws { @@ -33,8 +33,8 @@ extension SwiftLintDev.Reporters { .map { $0.replacingOccurrences(of: ".swift", with: ".self") } .joined(separator: ",\n") let builtInReportersFile = reportersDirectory.deletingLastPathComponent() - .appendingPathComponent("Models", isDirectory: true) - .appendingPathComponent("ReportersList.swift", isDirectory: false) + .appending(path: "Models", directoryHint: .isDirectory) + .appending(path: "ReportersList.swift", directoryHint: .notDirectory) try """ // GENERATED FILE. DO NOT EDIT! diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index c756734df4..4fec5c71f3 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -14,16 +14,16 @@ extension SwiftLintDev.Rules { ) private var rulesDirectory: URL { - URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent("Source", isDirectory: true) - .appendingPathComponent("SwiftLintBuiltInRules", isDirectory: true) - .appendingPathComponent("Rules", isDirectory: true) + URL(filePath: FileManager.default.currentDirectoryPath) + .appending(path: "Source", directoryHint: .isDirectory) + .appending(path: "SwiftLintBuiltInRules", directoryHint: .isDirectory) + .appending(path: "Rules", directoryHint: .isDirectory) } private var testsDirectory: URL { - URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent("Tests", isDirectory: true) - .appendingPathComponent("GeneratedTests", isDirectory: true) + URL(filePath: FileManager.default.currentDirectoryPath) + .appending(path: "Tests", directoryHint: .isDirectory) + .appending(path: "GeneratedTests", directoryHint: .isDirectory) } func run() throws { @@ -174,8 +174,8 @@ private extension SwiftLintDev.Rules.Register { .map { $0.replacingOccurrences(of: ".swift", with: ".self") } .joined(separator: ",\n") let builtInRulesFile = rulesDirectory.deletingLastPathComponent() - .appendingPathComponent("Models", isDirectory: true) - .appendingPathComponent("BuiltInRules.swift", isDirectory: false) + .appending(path: "Models", directoryHint: .isDirectory) + .appending(path: "BuiltInRules.swift", directoryHint: .notDirectory) let fileContent = generateBuiltInRulesFileContent(rulesImportList: rulesImportString) try fileContent.write(to: builtInRulesFile, atomically: true, encoding: .utf8) @@ -208,9 +208,9 @@ private extension SwiftLintDev.Rules.Register { }.joined(separator: "\n\n") let shardNumber = rulesContext.shardNumbers[shardIndex] - let testFile = testsDirectory.appendingPathComponent( - "GeneratedTests_\(shardNumber).swift", - isDirectory: false + let testFile = testsDirectory.appending( + path: "GeneratedTests_\(shardNumber).swift", + directoryHint: .notDirectory ) let fileContent = generateSwiftTestFileContent(forTestClasses: testClasses) @@ -231,9 +231,9 @@ private extension SwiftLintDev.Rules.Register { #""//Tests:GeneratedTests_\#($0)""# }.joined(separator: ",\n ") - let bzlFile = testsParentDirectory.appendingPathComponent( - "generated_tests.bzl", - isDirectory: false + let bzlFile = testsParentDirectory.appending( + path: "generated_tests.bzl", + directoryHint: .notDirectory ) let fileContent = generateBzlFileContent( @@ -268,9 +268,9 @@ private extension SwiftLintDev.Rules.Register { .appending("\n") .write( to: testsParentDirectory - .appendingPathComponent("IntegrationTests", isDirectory: true) - .appendingPathComponent("Resources", isDirectory: true) - .appendingPathComponent("default_rule_configurations.yml", isDirectory: false), + .appending(path: "IntegrationTests", directoryHint: .isDirectory) + .appending(path: "Resources", directoryHint: .isDirectory) + .appending(path: "default_rule_configurations.yml", directoryHint: .notDirectory), atomically: true, encoding: .utf8 ) diff --git a/Source/swiftlint-dev/Rules+Template.swift b/Source/swiftlint-dev/Rules+Template.swift index a88e29f3be..8cc87a2acc 100644 --- a/Source/swiftlint-dev/Rules+Template.swift +++ b/Source/swiftlint-dev/Rules+Template.swift @@ -43,17 +43,17 @@ extension SwiftLintDev.Rules { var skipRegistration = false func run() throws { - let rootDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + let rootDirectory = URL(filePath: FileManager.default.currentDirectoryPath) let ruleDirectory = rootDirectory - .appendingPathComponent("Source", isDirectory: true) - .appendingPathComponent("SwiftLintBuiltInRules", isDirectory: true) - .appendingPathComponent("Rules", isDirectory: true) - let ruleLocation = ruleDirectory.appendingPathComponent(kind.rawValue.capitalized, isDirectory: true) + .appending(path: "Source", directoryHint: .isDirectory) + .appending(path: "SwiftLintBuiltInRules", directoryHint: .isDirectory) + .appending(path: "Rules", directoryHint: .isDirectory) + let ruleLocation = ruleDirectory.appending(path: kind.rawValue.capitalized, directoryHint: .isDirectory) guard FileManager.default.fileExists(atPath: ruleLocation.filepath) else { throw ValidationError("Command must be run from the root of the SwiftLint repository.") } print("Creating template(s) for new rule \"\(ruleName)\" identified by '\(ruleId)' ...") - let rulePath = ruleLocation.appendingPathComponent("\(name)Rule.swift", isDirectory: false) + let rulePath = ruleLocation.appending(path: "\(name)Rule.swift", directoryHint: .notDirectory) guard overwrite || !FileManager.default.fileExists(atPath: rulePath.filepath) else { throw ValidationError("Rule file already exists at \(rulePath.relativeToCurrentDirectory).") } @@ -61,8 +61,8 @@ extension SwiftLintDev.Rules { print("Rule file created at \(rulePath.relativeToCurrentDirectory).") if config { let configPath = ruleDirectory - .appendingPathComponent("RuleConfigurations", isDirectory: true) - .appendingPathComponent("\(name)Configuration.swift", isDirectory: false) + .appending(path: "RuleConfigurations", directoryHint: .isDirectory) + .appending(path: "\(name)Configuration.swift", directoryHint: .notDirectory) guard overwrite || !FileManager.default.fileExists(atPath: configPath.filepath) else { throw ValidationError( "Configuration file already exists at \(configPath.relativeToCurrentDirectory)." @@ -73,9 +73,9 @@ extension SwiftLintDev.Rules { } if test { let testDirectory = rootDirectory - .appendingPathComponent("Tests", isDirectory: true) - .appendingPathComponent("BuiltInRulesTests", isDirectory: true) - let testPath = testDirectory.appendingPathComponent("\(name)RuleTests.swift", isDirectory: false) + .appending(path: "Tests", directoryHint: .isDirectory) + .appending(path: "BuiltInRulesTests", directoryHint: .isDirectory) + let testPath = testDirectory.appending(path: "\(name)RuleTests.swift", directoryHint: .notDirectory) guard FileManager.default.fileExists(atPath: testDirectory.filepath) else { throw ValidationError("Command must be run from the root of the SwiftLint repository.") } diff --git a/Source/swiftlint/Commands/Analyze.swift b/Source/swiftlint/Commands/Analyze.swift index 968fb99e27..0e81aa8824 100644 --- a/Source/swiftlint/Commands/Analyze.swift +++ b/Source/swiftlint/Commands/Analyze.swift @@ -1,4 +1,5 @@ import ArgumentParser +import Foundation import SwiftLintFramework extension SwiftLint { @@ -14,11 +15,11 @@ extension SwiftLint { @Option(help: "The path of a compilation database to use when running AnalyzerRules.") var compileCommands: String? @Argument(help: pathsArgumentDescription(for: .analyze)) - var paths = [String]() + var paths = [URL]() func run() async throws { // Analyze files in current working directory if no paths were specified. - let allPaths = paths.isNotEmpty ? paths : [""] + let allPaths = paths.isNotEmpty ? paths : [URL.cwd] let options = LintOrAnalyzeOptions( mode: .analyze, paths: allPaths, diff --git a/Source/swiftlint/Commands/Baseline.swift b/Source/swiftlint/Commands/Baseline.swift index f3170fca54..2a8515a82e 100644 --- a/Source/swiftlint/Commands/Baseline.swift +++ b/Source/swiftlint/Commands/Baseline.swift @@ -13,7 +13,7 @@ extension SwiftLint { private struct BaselineOptions: ParsableArguments { @Argument(help: "The path to the baseline file.") - var baseline: String + var baseline: URL } private struct ReportingOptions: ParsableArguments { @@ -25,7 +25,7 @@ extension SwiftLint { ) var reporter: String? @Option(help: "The file where violations should be saved. Prints to stdout by default.") - var output: String? + var output: URL? } private struct Report: ParsableCommand { @@ -56,7 +56,7 @@ extension SwiftLint { the second baseline that are not present in the original baseline will be reported. """ ) - var otherBaseline: String + var otherBaseline: URL @OptionGroup var reportingOptions: ReportingOptions @@ -68,14 +68,14 @@ extension SwiftLint { } } -private func report(_ violations: [StyleViolation], using reporterIdentifier: String?, to output: String?) { +private func report(_ violations: [StyleViolation], using reporterIdentifier: String?, to output: URL?) { let reporter = reporterFrom(identifier: reporterIdentifier) let report = reporter.generateReport(violations) if report.isNotEmpty { if let output { let data = Data((report + "\n").utf8) do { - try data.write(to: URL(fileURLWithPath: output)) + try data.write(to: output) } catch { Issue.fileNotWritable(path: output).print() } diff --git a/Source/swiftlint/Commands/Docs.swift b/Source/swiftlint/Commands/Docs.swift index c9c4fa90f4..af310189b6 100644 --- a/Source/swiftlint/Commands/Docs.swift +++ b/Source/swiftlint/Commands/Docs.swift @@ -29,12 +29,12 @@ extension SwiftLint { private func open(_ url: URL) { let process = Process() #if os(Linux) - process.executableURL = URL(fileURLWithPath: "/usr/bin/env", isDirectory: false) + process.executableURL = URL(filePath: "/usr/bin/env", directoryHint: .notDirectory) let command = "xdg-open" process.arguments = [command, url.absoluteString] try? process.run() #elseif os(Windows) - process.executableURL = URL(fileURLWithPath: "cmd", isDirectory: false) + process.executableURL = URL(filePath: "cmd", directoryHint: .notDirectory) process.arguments = ["/C", "start", url.absoluteString] try? process.run() #else diff --git a/Source/swiftlint/Commands/GenerateDocs.swift b/Source/swiftlint/Commands/GenerateDocs.swift index cd31b2d2d7..1875a121c0 100644 --- a/Source/swiftlint/Commands/GenerateDocs.swift +++ b/Source/swiftlint/Commands/GenerateDocs.swift @@ -11,7 +11,7 @@ extension SwiftLint { @Option(help: "The directory where the documentation should be saved") var path = "rule_docs" @Option(help: "The path to a SwiftLint configuration file") - var config: String? + var config: URL? @OptionGroup var rulesFilterOptions: RulesFilterOptions @@ -21,7 +21,7 @@ extension SwiftLint { let rules = rulesFilter.getRules(excluding: rulesFilterOptions.excludingOptions) try RuleListDocumentation(rules) - .write(to: URL(fileURLWithPath: path, isDirectory: true)) + .write(to: URL(filePath: path, directoryHint: .isDirectory)) } } } diff --git a/Source/swiftlint/Commands/Lint.swift b/Source/swiftlint/Commands/Lint.swift index 56c8cca192..d67a22625e 100644 --- a/Source/swiftlint/Commands/Lint.swift +++ b/Source/swiftlint/Commands/Lint.swift @@ -1,4 +1,5 @@ import ArgumentParser +import Foundation import SwiftLintFramework extension SwiftLint { @@ -25,7 +26,7 @@ extension SwiftLint { ) var disableSourceKit = false @Argument(help: pathsArgumentDescription(for: .lint)) - var paths = [String]() + var paths = [URL]() func run() async throws { Issue.printDeprecationWarnings = !silenceDeprecationWarnings @@ -35,7 +36,7 @@ extension SwiftLint { } // Lint files in current working directory if no paths were specified. - let allPaths = paths.isNotEmpty ? paths : [""] + let allPaths = paths.isNotEmpty ? paths : [URL.cwd] let options = LintOrAnalyzeOptions( mode: .lint, paths: allPaths, diff --git a/Source/swiftlint/Commands/Rules.swift b/Source/swiftlint/Commands/Rules.swift index 6c812c411b..4672badccd 100644 --- a/Source/swiftlint/Commands/Rules.swift +++ b/Source/swiftlint/Commands/Rules.swift @@ -13,7 +13,7 @@ extension SwiftLint { static let configuration = CommandConfiguration(abstract: "Display the list of rules and their identifiers") @Option(help: "The path to a SwiftLint configuration file") - var config: String? + var config: URL? @OptionGroup var rulesFilterOptions: RulesFilterOptions @Flag(name: .shortAndLong, help: "Display full configuration details") diff --git a/Source/swiftlint/Common/LintOrAnalyzeArguments.swift b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift index b47e4162a6..b316b0aaef 100644 --- a/Source/swiftlint/Common/LintOrAnalyzeArguments.swift +++ b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift @@ -19,7 +19,7 @@ enum LeniencyOptions: String, EnumerableFlag { struct LintOrAnalyzeArguments: ParsableArguments { @Option(help: "The path to one or more SwiftLint configuration files, evaluated as a parent-child hierarchy.") - var config = [String]() + var config = [URL]() @Flag(name: [.long, .customLong("autocorrect")], help: "Correct violations whenever possible.") var fix = false @@ -43,13 +43,13 @@ struct LintOrAnalyzeArguments: ParsableArguments { @Option(help: "The reporter used to log errors and warnings.") var reporter: String? @Option(help: "The path to a baseline file, which will be used to filter out detected violations.") - var baseline: String? + var baseline: URL? @Option(help: "The path to save detected violations to as a new baseline.") - var writeBaseline: String? + var writeBaseline: URL? @Option(help: "The working directory to use when running SwiftLint.") var workingDirectory: String? @Option(help: "The file where violations should be saved. Prints to stdout by default.") - var output: String? + var output: URL? @Flag(help: "Show a live-updating progress bar instead of each file being processed.") var progress = false @Flag(help: "Check whether a later version of SwiftLint is available after processing all files.") @@ -76,3 +76,9 @@ func pathsArgumentDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp { func quietOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp { "Don't print status logs like '\(mode.verb.capitalized) ' & 'Done \(mode.verb)'." } + +extension URL: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(filePath: argument) + } +} diff --git a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift index 3ce1b5e6b1..de098c13c3 100644 --- a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift @@ -2,13 +2,13 @@ import TestHelpers import XCTest -private let fixturesDirectory = "\(TestResources.path().filepath)/FileHeaderRuleFixtures" - final class FileHeaderRuleTests: SwiftLintTestCase { private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] { - let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! + let file = TestResources.path() + .appending(path: "FileHeaderRuleFixtures", directoryHint: .isDirectory) + .appending(path: fileName, directoryHint: .notDirectory) let rule = try FileHeaderRule(configuration: configuration) - return rule.validate(file: file) + return rule.validate(file: SwiftLintFile(path: file)!) } func testFileHeaderWithDefaultConfiguration() { diff --git a/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift b/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift index 1d142807c1..c61e1fedb8 100644 --- a/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameNoSpaceRuleTests.swift @@ -3,19 +3,18 @@ import SourceKittenFramework import TestHelpers import XCTest -private let fixturesDirectory = "\(TestResources.path().filepath)/FileNameNoSpaceRuleFixtures" - final class FileNameNoSpaceRuleTests: SwiftLintTestCase { private func validate(fileName: String, excludedOverride: [String]? = nil) throws -> [StyleViolation] { - let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! - let rule: FileNameNoSpaceRule - if let excluded = excludedOverride { - rule = try FileNameNoSpaceRule(configuration: ["excluded": excluded]) - } else { - rule = FileNameNoSpaceRule() - } - - return rule.validate(file: file) + let file = TestResources.path() + .appending(path: "FileNameNoSpaceRuleFixtures", directoryHint: .isDirectory) + .appending(path: fileName, directoryHint: .notDirectory) + let rule = + if let excluded = excludedOverride { + try FileNameNoSpaceRule(configuration: ["excluded": excluded]) + } else { + FileNameNoSpaceRule() + } + return rule.validate(file: SwiftLintFile(path: file)!) } func testFileNameDoesntTrigger() { diff --git a/Tests/BuiltInRulesTests/FileNameRuleTests.swift b/Tests/BuiltInRulesTests/FileNameRuleTests.swift index 80c6db6d2a..eb962bc5fb 100644 --- a/Tests/BuiltInRulesTests/FileNameRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameRuleTests.swift @@ -2,8 +2,7 @@ import TestHelpers import XCTest -private let fixturesDirectory = - TestResources.path().appendingPathComponent("FileNameRuleFixtures").filepath +private let fixturesDirectory = TestResources.path().appending(path: "FileNameRuleFixtures") final class FileNameRuleTests: SwiftLintTestCase { private func validate(fileName: String, @@ -13,7 +12,7 @@ final class FileNameRuleTests: SwiftLintTestCase { suffixPattern: String? = nil, nestedTypeSeparator: String? = nil, requireFullyQualifiedNames: Bool = false) throws -> [StyleViolation] { - let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! + let file = SwiftLintFile(path: fixturesDirectory.appending(path: fileName))! var configuration = [String: Any]() diff --git a/Tests/CoreTests/RegexConfigurationTests.swift b/Tests/CoreTests/RegexConfigurationTests.swift index c0e0dca1ab..8179850c4d 100644 --- a/Tests/CoreTests/RegexConfigurationTests.swift +++ b/Tests/CoreTests/RegexConfigurationTests.swift @@ -5,33 +5,33 @@ import XCTest final class RegexConfigurationTests: SwiftLintTestCase { func testShouldValidateIsTrueByDefault() { let config = RegexConfiguration(identifier: "example") - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) } - func testShouldValidateWithSingleExluded() throws { + func testShouldValidateWithSingleExcluded() throws { var config = RegexConfiguration(identifier: "example") try config.apply(configuration: [ "regex": "try!", - "excluded": "Tests/.*\\.swift", + "excluded": "ExcludedFolder/.*\\.swift", ]) - XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) + XCTAssertFalse(config.shouldValidate(filePath: "ExcludedFolder/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) } - func testShouldValidateWithArrayExluded() throws { + func testShouldValidateWithArrayExcluded() throws { var config = RegexConfiguration(identifier: "example") try config.apply(configuration: [ "regex": "try!", "excluded": [ - "^Tests/.*\\.swift", - "^MyFramework/Tests/.*\\.swift", + "ExcludedFolder/.*\\.swift", + "MyFramework/ExcludedFolder/.*\\.swift", ] as Any, ]) - XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) - XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/Tests/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) + XCTAssertFalse(config.shouldValidate(filePath: "ExcludedFolder/file.swift".url())) + XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/ExcludedFolder/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) } func testShouldValidateWithSingleIncluded() throws { @@ -41,9 +41,9 @@ final class RegexConfigurationTests: SwiftLintTestCase { "included": "App/.*\\.swift", ]) - XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) - XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/Tests/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) + XCTAssertFalse(config.shouldValidate(filePath: "ExcludedFolder/file.swift".url())) + XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/ExcludedFolder/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) } func testShouldValidateWithArrayIncluded() throws { @@ -56,9 +56,9 @@ final class RegexConfigurationTests: SwiftLintTestCase { ] as Any, ]) - XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "MyFramework/file.swift")) + XCTAssertFalse(config.shouldValidate(filePath: "ExcludedFolder/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "MyFramework/file.swift".url())) } func testShouldValidateWithIncludedAndExcluded() throws { @@ -70,16 +70,16 @@ final class RegexConfigurationTests: SwiftLintTestCase { "MyFramework/.*\\.swift", ] as Any, "excluded": [ - "Tests/.*\\.swift", + "ExcludedFolder/.*\\.swift", "App/Fixtures/.*\\.swift", ] as Any, ]) - XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) - XCTAssertTrue(config.shouldValidate(filePath: "MyFramework/file.swift")) + XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift".url())) + XCTAssertTrue(config.shouldValidate(filePath: "MyFramework/file.swift".url())) - XCTAssertFalse(config.shouldValidate(filePath: "App/Fixtures/file.swift")) - XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) - XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/Tests/file.swift")) + XCTAssertFalse(config.shouldValidate(filePath: "App/Fixtures/file.swift".url())) + XCTAssertFalse(config.shouldValidate(filePath: "ExcludedFolder/file.swift".url())) + XCTAssertFalse(config.shouldValidate(filePath: "MyFramework/ExcludedFolder/file.swift".url())) } } diff --git a/Tests/CoreTests/SwiftLintFileTests.swift b/Tests/CoreTests/SwiftLintFileTests.swift index ff6e860d42..ae689a5190 100644 --- a/Tests/CoreTests/SwiftLintFileTests.swift +++ b/Tests/CoreTests/SwiftLintFileTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import SwiftLintCore final class SwiftLintFileTests: SwiftLintTestCase { - private let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + private let tempFile = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) override func setUp() async throws { try await super.setUp() @@ -34,7 +34,7 @@ final class SwiftLintFileTests: SwiftLintTestCase { } func testFileUpdate() { - let file = SwiftLintFile(path: tempFile.path)! + let file = SwiftLintFile(path: tempFile)! XCTAssertFalse(file.isVirtual) XCTAssertNotNil(file.path) @@ -52,20 +52,20 @@ final class SwiftLintFileTests: SwiftLintTestCase { } func testFileNotTouchedIfNothingAppended() { - let file = SwiftLintFile(path: tempFile.path)! - let initialModificationData = FileManager.default.modificationDate(forFileAtPath: tempFile.path) + let file = SwiftLintFile(path: tempFile)! + let initialModificationData = FileManager.default.modificationDate(forFileAtPath: tempFile) file.append("") - XCTAssertEqual(initialModificationData, FileManager.default.modificationDate(forFileAtPath: tempFile.path)) + XCTAssertEqual(initialModificationData, FileManager.default.modificationDate(forFileAtPath: tempFile)) } func testFileNotTouchedIfNothingNewWritten() { - let file = SwiftLintFile(path: tempFile.path)! - let initialModificationData = FileManager.default.modificationDate(forFileAtPath: tempFile.path) + let file = SwiftLintFile(path: tempFile)! + let initialModificationData = FileManager.default.modificationDate(forFileAtPath: tempFile) file.write("let i = 2") - XCTAssertEqual(initialModificationData, FileManager.default.modificationDate(forFileAtPath: tempFile.path)) + XCTAssertEqual(initialModificationData, FileManager.default.modificationDate(forFileAtPath: tempFile)) } } diff --git a/Tests/CoreTests/YamlSwiftLintTests.swift b/Tests/CoreTests/YamlSwiftLintTests.swift index 89dcef9707..9b53f7ad02 100644 --- a/Tests/CoreTests/YamlSwiftLintTests.swift +++ b/Tests/CoreTests/YamlSwiftLintTests.swift @@ -41,6 +41,9 @@ final class YamlSwiftLintTests: SwiftLintTestCase { } private func getTestYaml() throws -> String { - try String(contentsOfFile: "\(TestResources.path().filepath)/test.yml", encoding: .utf8) + try String( + contentsOf: TestResources.path().appending(path: "test.yml", directoryHint: .notDirectory), + encoding: .utf8 + ) } } diff --git a/Tests/FileSystemAccessTests/BaselineTests.swift b/Tests/FileSystemAccessTests/BaselineTests.swift index 466976fc88..79b8aa4437 100644 --- a/Tests/FileSystemAccessTests/BaselineTests.swift +++ b/Tests/FileSystemAccessTests/BaselineTests.swift @@ -5,19 +5,6 @@ import XCTest @testable import SwiftLintBuiltInRules @testable import SwiftLintCore -private var temporaryDirectoryPath: String { - let result = URL( - fileURLWithPath: NSTemporaryDirectory(), - isDirectory: true - ).filepath - -#if os(macOS) - return "/private" + result -#else - return result -#endif -} - final class BaselineTests: XCTestCase { private static let example = """ import Foundation @@ -50,11 +37,11 @@ final class BaselineTests: XCTestCase { DirectReturnRule.description, ] - private static func violations(for filePath: String?) -> [StyleViolation] { + private static func violations(for filePath: URL?) -> [StyleViolation] { ruleDescriptions.violations(for: filePath) } - private static func baseline(for filePath: String) -> Baseline { + private static func baseline(for filePath: URL) -> Baseline { Baseline(violations: ruleDescriptions.violations(for: filePath)) } @@ -63,7 +50,7 @@ final class BaselineTests: XCTestCase { override static func setUp() { super.setUp() currentDirectoryPath = FileManager.default.currentDirectoryPath - XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(temporaryDirectoryPath)) + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(URL.temporaryDirectory.filepath)) } override static func tearDown() { @@ -73,10 +60,10 @@ final class BaselineTests: XCTestCase { func testWritingAndReading() throws { try withExampleFileCreated { sourceFilePath in - let baselinePath = temporaryDirectoryPath.stringByAppendingPathComponent(UUID().uuidString) + let baselinePath = URL.temporaryDirectory.appending(path: UUID().uuidString) try Baseline(violations: Self.violations(for: sourceFilePath)).write(toPath: baselinePath) defer { - try? FileManager.default.removeItem(atPath: baselinePath) + try? FileManager.default.removeItem(atPath: baselinePath.filepath) } let newBaseline = try Baseline(fromPath: baselinePath) XCTAssertEqual(newBaseline, Self.baseline(for: sourceFilePath)) @@ -176,22 +163,22 @@ final class BaselineTests: XCTestCase { } } - private func withExampleFileCreated(_ block: (String) throws -> Void) throws { - let sourceFilePath = temporaryDirectoryPath.stringByAppendingPathComponent("\(UUID().uuidString).swift") + private func withExampleFileCreated(_ block: (URL) throws -> Void) throws { + let sourceFilePath = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).swift") guard let data = Self.example.data(using: .utf8) else { XCTFail("Could not convert example code to data using UTF-8 encoding") return } - try data.write(to: URL(fileURLWithPath: sourceFilePath)) + try data.write(to: sourceFilePath) defer { - try? FileManager.default.removeItem(atPath: sourceFilePath) + try? FileManager.default.removeItem(atPath: sourceFilePath.filepath) } try block(sourceFilePath) } } private extension [StyleViolation] { - func lineShifted(by shift: Int, path: String) throws -> [StyleViolation] { + func lineShifted(by shift: Int, path: URL) throws -> [StyleViolation] { guard shift > 0 else { XCTFail("Shift must be positive") return self @@ -199,7 +186,7 @@ private extension [StyleViolation] { var lines = SwiftLintFile(path: path)?.lines.map(\.content) ?? [] lines = [String](repeating: "", count: shift) + lines if let data = lines.joined(separator: "\n").data(using: .utf8) { - try data.write(to: URL(fileURLWithPath: path)) + try data.write(to: path) } return map { let shiftedLocation = Location( @@ -213,7 +200,7 @@ private extension [StyleViolation] { } private extension Sequence where Element == RuleDescription { - func violations(for filePath: String?) -> [StyleViolation] { + func violations(for filePath: URL?) -> [StyleViolation] { enumerated().map { index, ruleDescription in StyleViolation( ruleDescription: ruleDescription, diff --git a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift index 796f31916e..a57bd06e30 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift @@ -3,88 +3,86 @@ import SwiftLintFramework import TestHelpers // swiftlint:disable:next blanket_disable_command -// swiftlint:disable nesting identifier_name +// swiftlint:disable identifier_name -internal extension ConfigurationTests { - enum Mock { - // MARK: Test Resources Path - static let testResourcesPath: String = TestResources.path().filepath +enum Mock { + // MARK: Test Resources Path + static let testResourcesPath = TestResources.path() - // MARK: Directory Paths - enum Dir { - static var level0: String { testResourcesPath.stringByAppendingPathComponent("ProjectMock") } - static var level1: String { level0.stringByAppendingPathComponent("Level1") } - static var level2: String { level1.stringByAppendingPathComponent("Level2") } - static var level3: String { level2.stringByAppendingPathComponent("Level3") } - static var nested: String { level0.stringByAppendingPathComponent("NestedConfig/Test") } - static var nestedSub: String { nested.stringByAppendingPathComponent("Sub") } - static var childConfigTest1: String { level0.stringByAppendingPathComponent("ChildConfig/Test1/Main") } - static var childConfigTest2: String { level0.stringByAppendingPathComponent("ChildConfig/Test2") } - static var childConfigCycle1: String { level0.stringByAppendingPathComponent("ChildConfig/Cycle1") } - static var childConfigCycle2: String { level0.stringByAppendingPathComponent("ChildConfig/Cycle2") } - static var childConfigCycle3: String { level0.stringByAppendingPathComponent("ChildConfig/Cycle3/Main") } - static var childConfigCycle4: String { level0.stringByAppendingPathComponent("ChildConfig/Cycle4") } - static var parentConfigTest1: String { level0.stringByAppendingPathComponent("ParentConfig/Test1") } - static var parentConfigTest2: String { level0.stringByAppendingPathComponent("ParentConfig/Test2") } - static var parentConfigCycle1: String { level0.stringByAppendingPathComponent("ParentConfig/Cycle1") } - static var parentConfigCycle2: String { level0.stringByAppendingPathComponent("ParentConfig/Cycle2") } - static var parentConfigCycle3: String { level0.stringByAppendingPathComponent("ParentConfig/Cycle3") } - static var remoteConfigChild: String { level0.stringByAppendingPathComponent("RemoteConfig/Child") } - static var remoteConfigParent: String { level0.stringByAppendingPathComponent("RemoteConfig/Parent") } - static var remoteConfigLocalRef: String { level0.stringByAppendingPathComponent("RemoteConfig/LocalRef") } - static var remoteConfigCycle: String { level0.stringByAppendingPathComponent("RemoteConfig/Cycle") } - static var emptyFolder: String { level0.stringByAppendingPathComponent("EmptyFolder") } + // MARK: Directory Paths + enum Dir { + static var level0: URL { testResourcesPath.appending(path: "ProjectMock/") } + static var level1: URL { level0.appending(path: "Level1/") } + static var level2: URL { level1.appending(path: "Level2/") } + static var level3: URL { level2.appending(path: "Level3/") } + static var nested: URL { level0.appending(path: "NestedConfig/Test/") } + static var nestedSub: URL { nested.appending(path: "Sub/") } + static var childConfigTest1: URL { level0.appending(path: "ChildConfig/Test1/Main/") } + static var childConfigTest2: URL { level0.appending(path: "ChildConfig/Test2/") } + static var childConfigCycle1: URL { level0.appending(path: "ChildConfig/Cycle1/") } + static var childConfigCycle2: URL { level0.appending(path: "ChildConfig/Cycle2/") } + static var childConfigCycle3: URL { level0.appending(path: "ChildConfig/Cycle3/Main/") } + static var childConfigCycle4: URL { level0.appending(path: "ChildConfig/Cycle4/") } + static var parentConfigTest1: URL { level0.appending(path: "ParentConfig/Test1/") } + static var parentConfigTest2: URL { level0.appending(path: "ParentConfig/Test2/") } + static var parentConfigCycle1: URL { level0.appending(path: "ParentConfig/Cycle1/") } + static var parentConfigCycle2: URL { level0.appending(path: "ParentConfig/Cycle2/") } + static var parentConfigCycle3: URL { level0.appending(path: "ParentConfig/Cycle3/") } + static var remoteConfigChild: URL { level0.appending(path: "RemoteConfig/Child/") } + static var remoteConfigParent: URL { level0.appending(path: "RemoteConfig/Parent/") } + static var remoteConfigLocalRef: URL { level0.appending(path: "RemoteConfig/LocalRef/") } + static var remoteConfigCycle: URL { level0.appending(path: "RemoteConfig/Cycle/") } + static var emptyFolder: URL { level0.appending(path: "EmptyFolder/") } - static var exclusionTests: String { testResourcesPath.stringByAppendingPathComponent("ExclusionTests") } - static var directory: String { exclusionTests.stringByAppendingPathComponent("directory") } - static var directoryExcluded: String { directory.stringByAppendingPathComponent("excluded") } - } + static var exclusionTests: URL { testResourcesPath.appending(path: "ExclusionTests/") } + static var directory: URL { exclusionTests.appending(path: "directory/") } + static var directoryExcluded: URL { directory.appending(path: "excluded/") } + } - // MARK: YAML File Paths - enum Yml { - static var _0: String { Dir.level0.stringByAppendingPathComponent(Configuration.defaultFileName) } - static var _0Custom: String { Dir.level0.stringByAppendingPathComponent("custom.yml") } - static var _0CustomRules: String { Dir.level0.stringByAppendingPathComponent("custom_rules.yml") } - static var _0CustomRulesOnly: String { Dir.level0.stringByAppendingPathComponent("custom_rules_only.yml") } - static var _2: String { Dir.level2.stringByAppendingPathComponent(Configuration.defaultFileName) } - static var _2CustomRules: String { Dir.level2.stringByAppendingPathComponent("custom_rules.yml") } - static var _2CustomRulesOnly: String { Dir.level2.stringByAppendingPathComponent("custom_rules_only.yml") } - static var _2CustomRulesDisabled: String { - Dir.level2.stringByAppendingPathComponent("custom_rules_disabled.yml") - } - static var _2CustomRulesReconfig: String { - Dir.level2.stringByAppendingPathComponent("custom_rules_reconfig.yml") - } - static var _3: String { Dir.level3.stringByAppendingPathComponent(Configuration.defaultFileName) } - static var nested: String { Dir.nested.stringByAppendingPathComponent(Configuration.defaultFileName) } + // MARK: YAML File Paths + enum Yml { + static var _0: URL { Dir.level0.appending(path: Configuration.defaultFileName) } + static var _0Custom: URL { Dir.level0.appending(path: "custom.yml") } + static var _0CustomRules: URL { Dir.level0.appending(path: "custom_rules.yml") } + static var _0CustomRulesOnly: URL { Dir.level0.appending(path: "custom_rules_only.yml") } + static var _2: URL { Dir.level2.appending(path: Configuration.defaultFileName) } + static var _2CustomRules: URL { Dir.level2.appending(path: "custom_rules.yml") } + static var _2CustomRulesOnly: URL { Dir.level2.appending(path: "custom_rules_only.yml") } + static var _2CustomRulesDisabled: URL { + Dir.level2.appending(path: "custom_rules_disabled.yml") } - - // MARK: Swift File Paths - enum Swift { - static var _0: String { Dir.level0.stringByAppendingPathComponent("Level0.swift") } - static var _1: String { Dir.level1.stringByAppendingPathComponent("Level1.swift") } - static var _2: String { Dir.level2.stringByAppendingPathComponent("Level2.swift") } - static var _3: String { Dir.level3.stringByAppendingPathComponent("Level3.swift") } - static var nestedSub: String { Dir.nestedSub.stringByAppendingPathComponent("Sub.swift") } + static var _2CustomRulesReconfig: URL { + Dir.level2.appending(path: "custom_rules_reconfig.yml") } + static var _3: URL { Dir.level3.appending(path: Configuration.defaultFileName) } + static var nested: URL { Dir.nested.appending(path: Configuration.defaultFileName) } + } - // MARK: Configurations - enum Config { - static var _0: Configuration { Configuration(configurationFiles: []) } - static var _0Custom: Configuration { Configuration(configurationFiles: [Yml._0Custom]) } - static var _0CustomRules: Configuration { Configuration(configurationFiles: [Yml._0CustomRules]) } - static var _0CustomRulesOnly: Configuration { Configuration(configurationFiles: [Yml._0CustomRulesOnly]) } - static var _2: Configuration { Configuration(configurationFiles: [Yml._2]) } - static var _2CustomRules: Configuration { Configuration(configurationFiles: [Yml._2CustomRules]) } - static var _2CustomRulesOnly: Configuration { Configuration(configurationFiles: [Yml._2CustomRulesOnly]) } - static var _2CustomRulesDisabled: Configuration { - Configuration(configurationFiles: [Yml._2CustomRulesDisabled]) - } - static var _2CustomRulesReconfig: Configuration { - Configuration(configurationFiles: [Yml._2CustomRulesReconfig]) - } - static var _3: Configuration { Configuration(configurationFiles: [Yml._3]) } - static var nested: Configuration { Configuration(configurationFiles: [Yml.nested]) } + // MARK: Swift File Paths + enum Swift { + static var _0: URL { Dir.level0.appending(path: "Level0.swift") } + static var _1: URL { Dir.level1.appending(path: "Level1.swift") } + static var _2: URL { Dir.level2.appending(path: "Level2.swift") } + static var _3: URL { Dir.level3.appending(path: "Level3.swift") } + static var nestedSub: URL { Dir.nestedSub.appending(path: "Sub.swift") } + } + + // MARK: Configurations + enum Config { + static var _0: Configuration { Configuration(configurationFiles: []) } + static var _0Custom: Configuration { Configuration(configurationFiles: [Yml._0Custom]) } + static var _0CustomRules: Configuration { Configuration(configurationFiles: [Yml._0CustomRules]) } + static var _0CustomRulesOnly: Configuration { Configuration(configurationFiles: [Yml._0CustomRulesOnly]) } + static var _2: Configuration { Configuration(configurationFiles: [Yml._2]) } + static var _2CustomRules: Configuration { Configuration(configurationFiles: [Yml._2CustomRules]) } + static var _2CustomRulesOnly: Configuration { Configuration(configurationFiles: [Yml._2CustomRulesOnly]) } + static var _2CustomRulesDisabled: Configuration { + Configuration(configurationFiles: [Yml._2CustomRulesDisabled]) + } + static var _2CustomRulesReconfig: Configuration { + Configuration(configurationFiles: [Yml._2CustomRulesReconfig]) } + static var _3: Configuration { Configuration(configurationFiles: [Yml._3]) } + static var nested: Configuration { Configuration(configurationFiles: [Yml.nested]) } } } diff --git a/Tests/FileSystemAccessTests/ConfigurationTests.swift b/Tests/FileSystemAccessTests/ConfigurationTests.swift index 96a6ecf826..f228ab2364 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests.swift @@ -9,7 +9,7 @@ import XCTest private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0) -final class ConfigurationTests: SwiftLintTestCase { +final class ConfigurationTests: SwiftLintTestCase { // swiftlint:disable:this type_body_length // MARK: Setup & Teardown private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional @@ -17,7 +17,7 @@ final class ConfigurationTests: SwiftLintTestCase { super.setUp() Configuration.resetCache() previousWorkingDir = FileManager.default.currentDirectoryPath - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) } override func tearDown() { @@ -39,7 +39,7 @@ final class ConfigurationTests: SwiftLintTestCase { func testNoConfiguration() { // Change to a folder where there is no `.swiftlint.yml` - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder.filepath)) // Test whether the default configuration is used if there is no `.swiftlint.yml` or other config file XCTAssertEqual(Configuration(configurationFiles: []), Configuration.default) @@ -67,7 +67,7 @@ final class ConfigurationTests: SwiftLintTestCase { func testInitWithRelativePathAndRootPath() { let expectedConfig = Mock.Config._0 - let config = Configuration(configurationFiles: [".swiftlint.yml"]) + let config = Configuration(configurationFiles: [".swiftlint.yml".url()]) XCTAssertEqual(config.rulesWrapper.disabledRuleIdentifiers, expectedConfig.rulesWrapper.disabledRuleIdentifiers) XCTAssertEqual(config.includedPaths, expectedConfig.includedPaths) @@ -76,8 +76,8 @@ final class ConfigurationTests: SwiftLintTestCase { XCTAssertEqual(config.reporter, expectedConfig.reporter) XCTAssertTrue(config.allowZeroLintableFiles) XCTAssertTrue(config.strict) - XCTAssertNotNil(config.baseline) - XCTAssertNotNil(config.writeBaseline) + XCTAssertEqual(config.baseline, expectedConfig.baseline) + XCTAssertEqual(config.writeBaseline, expectedConfig.writeBaseline) } func testEnableAllRulesConfiguration() throws { @@ -248,48 +248,44 @@ final class ConfigurationTests: SwiftLintTestCase { return } - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1.filepath)) // The included path "File.swift" should be put relative to the configuration file // (~> Resources/ProjectMock/File.swift) and not relative to the path where // SwiftLint is run from (~> Resources/ProjectMock/Level1/File.swift) - let configuration = Configuration(configurationFiles: ["../custom_included_excluded.yml"]) - let actualIncludedPath = configuration.includedPaths.first!.bridge() - .absolutePathRepresentation(rootDirectory: configuration.rootDirectory) - let desiredIncludedPath = "File1.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0) - let actualExcludedPath = configuration.excludedPaths.first!.bridge() - .absolutePathRepresentation(rootDirectory: configuration.rootDirectory) - let desiredExcludedPath = "File2.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0) + let configuration = Configuration(configurationFiles: ["../custom_included_excluded.yml".url()]) + let actualIncludedPath = configuration.includedPaths.first! + let desiredIncludedPath = "File1.swift".url(relativeTo: Mock.Dir.level0) + let actualExcludedPath = configuration.excludedPaths.first! + let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0) - XCTAssertEqual(actualIncludedPath, desiredIncludedPath) - XCTAssertEqual(actualExcludedPath, desiredExcludedPath) + XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path) + XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path) } func testIncludedExcludedRelativeLocationLevel0() { // Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(), // but run from the directory the config file resides in - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) - let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml"]) - let actualIncludedPath = configuration.includedPaths.first!.bridge() - .absolutePathRepresentation(rootDirectory: configuration.rootDirectory) - let desiredIncludedPath = "File1.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0) - let actualExcludedPath = configuration.excludedPaths.first!.bridge() - .absolutePathRepresentation(rootDirectory: configuration.rootDirectory) - let desiredExcludedPath = "File2.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml".url()]) + let actualIncludedPath = configuration.includedPaths.first! + let desiredIncludedPath = "File1.swift".url(relativeTo: Mock.Dir.level0) + let actualExcludedPath = configuration.excludedPaths.first! + let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0) - XCTAssertEqual(actualIncludedPath, desiredIncludedPath) - XCTAssertEqual(actualExcludedPath, desiredExcludedPath) + XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path) + XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path) } func testExcludedPaths() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) let configuration = Configuration( - includedPaths: ["directory"], - excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"] + includedPaths: ["directory".url()], + excludedPaths: ["directory/excluded".url(), "directory/ExcludedFile.swift".url()] ) let paths = configuration.lintablePaths( - inPath: "", + inPath: URL.cwd, forceExclude: false, excludeByPrefix: false ) @@ -298,11 +294,11 @@ final class ConfigurationTests: SwiftLintTestCase { } func testForceExcludesFile() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) - let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) + let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()]) let paths = configuration.lintablePaths( - inPath: "directory/ExcludedFile.swift", + inPath: "directory/ExcludedFile.swift".url(), forceExclude: true, excludeByPrefix: false ) @@ -311,14 +307,14 @@ final class ConfigurationTests: SwiftLintTestCase { } func testForceExcludesFileNotPresentInExcluded() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) let configuration = Configuration( - includedPaths: ["directory"], - excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"] + includedPaths: ["directory".url()], + excludedPaths: ["directory/ExcludedFile.swift".url(), "directory/excluded".url()] ) let paths = configuration.lintablePaths( - inPath: "", + inPath: URL.cwd, forceExclude: true, excludeByPrefix: false ) @@ -327,11 +323,11 @@ final class ConfigurationTests: SwiftLintTestCase { } func testForceExcludesDirectory() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) - let configuration = Configuration(excludedPaths: ["directory/excluded"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) + let configuration = Configuration(excludedPaths: ["directory/excluded".url()]) let paths = configuration.lintablePaths( - inPath: "directory", + inPath: "directory".url(), forceExclude: true, excludeByPrefix: false ) @@ -340,11 +336,11 @@ final class ConfigurationTests: SwiftLintTestCase { } func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) - let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) + let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()]) let paths = configuration.lintablePaths( - inPath: "directory", + inPath: "directory".url(), forceExclude: true, excludeByPrefix: false ) @@ -356,7 +352,7 @@ final class ConfigurationTests: SwiftLintTestCase { let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, excludeByPrefix: false) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() let expectedFilenames = [ "DirectoryLevel1.swift", "Level0.swift", "Level1.swift", "Level2.swift", "Level3.swift", @@ -367,24 +363,24 @@ final class ConfigurationTests: SwiftLintTestCase { } func testGlobIncludePaths() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) - let configuration = Configuration(includedPaths: ["**/Level2"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + let configuration = Configuration(includedPaths: ["**/Level2".url()]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, excludeByPrefix: false) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] XCTAssertEqual(Set(expectedFilenames), Set(filenames)) } func testDuplicatedGlobIncludePaths() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) - let configuration = Configuration(includedPaths: ["**/Level2", "**/Level2"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + let configuration = Configuration(includedPaths: ["**/Level2".url(), "**/Level2".url()]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, excludeByPrefix: false) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] XCTAssertEqual(expectedFilenames, filenames) @@ -393,10 +389,10 @@ final class ConfigurationTests: SwiftLintTestCase { func testGlobExcludePaths() { let configuration = Configuration( includedPaths: [Mock.Dir.level3], - excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")] + excludedPaths: [Mock.Dir.level3.appending(path: "*.swift")] ) - let lintablePaths = configuration.lintablePaths(inPath: "", + let lintablePaths = configuration.lintablePaths(inPath: URL.cwd, forceExclude: false, excludeByPrefix: false) XCTAssertEqual(lintablePaths, []) @@ -488,93 +484,101 @@ final class ConfigurationTests: SwiftLintTestCase { func testBaseline() throws { let baselinePath = "Baseline.json" let configuration = try Configuration(dict: ["baseline": baselinePath]) - XCTAssertEqual(configuration.baseline, baselinePath) + XCTAssertEqual(configuration.baseline, baselinePath.url()) } func testWriteBaseline() throws { let baselinePath = "Baseline.json" let configuration = try Configuration(dict: ["write_baseline": baselinePath]) - XCTAssertEqual(configuration.writeBaseline, baselinePath) + XCTAssertEqual(configuration.writeBaseline, baselinePath.url()) } func testCheckForUpdates() throws { let configuration = try Configuration(dict: ["check_for_updates": true]) XCTAssertTrue(configuration.checkForUpdates) } -} -// MARK: - ExcludeByPrefix option tests -extension ConfigurationTests { + // MARK: - ExcludeByPrefix option tests + func testExcludeByPrefixExcludedPaths() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( - includedPaths: ["Level1"], - excludedPaths: ["Level1/Level1.swift", "Level1/Level2/Level3"] + includedPaths: ["Level1".url()], + excludedPaths: ["Level1/Level1.swift".url(), "Level1/Level2/Level3".url()] ) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, excludeByPrefix: true) - let filenames = paths.map { $0.bridge().lastPathComponent } + let filenames = paths.map(\.lastPathComponent) XCTAssertEqual(filenames, ["Level2.swift"]) } func testExcludeByPrefixForceExcludesFile() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) - let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"]) - let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift", + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift".url()]) + let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift".url(), forceExclude: true, excludeByPrefix: true) XCTAssertEqual([], paths) } func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) - let configuration = Configuration(includedPaths: ["Level1"], - excludedPaths: ["Level1/Level1.swift"]) - let paths = configuration.lintablePaths(inPath: "Level1", + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + let configuration = Configuration(includedPaths: ["Level1".url()], + excludedPaths: ["Level1/Level1.swift".url()]) + let paths = configuration.lintablePaths(inPath: "Level1".url(), forceExclude: true, excludeByPrefix: true) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } func testExcludeByPrefixForceExcludesDirectory() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( excludedPaths: [ - "Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig" + "Level1/Level2".url(), + "Directory.swift".url(), + "ChildConfig".url(), + "ParentConfig".url(), + "NestedConfig".url(), ] ) - let paths = configuration.lintablePaths(inPath: ".", + let paths = configuration.lintablePaths(inPath: URL.cwd, forceExclude: true, excludeByPrefix: true) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( excludedPaths: [ - "Level1", "Directory.swift/DirectoryLevel1.swift", "ChildConfig", "ParentConfig", "NestedConfig" + "Level1".url(), + "Directory.swift/DirectoryLevel1.swift".url(), + "ChildConfig".url(), + "ParentConfig".url(), + "NestedConfig".url(), ] ) - let paths = configuration.lintablePaths(inPath: ".", + let paths = configuration.lintablePaths(inPath: URL.cwd, forceExclude: true, excludeByPrefix: true) - let filenames = paths.map { $0.bridge().lastPathComponent } + let filenames = paths.map(\.lastPathComponent) XCTAssertEqual(["Level0.swift"], filenames) } func testExcludeByPrefixGlobExcludePaths() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( - includedPaths: ["Level1"], - excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) - let paths = configuration.lintablePaths(inPath: "Level1", + includedPaths: ["Level1".url()], + excludedPaths: ["Level1/*/*.swift".url(), "Level1/*/*/*.swift".url()] + ) + let paths = configuration.lintablePaths(inPath: "Level1".url(), forceExclude: false, excludeByPrefix: true) - let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() + let filenames = paths.map(\.lastPathComponent).sorted() XCTAssertEqual(filenames, ["Level1.swift"]) } @@ -625,12 +629,12 @@ extension ConfigurationTests { } private func assertEqual(_ relativeExpectedPaths: [String], - _ actualPaths: [String], + _ actualPaths: [URL], file: StaticString = #filePath, line: UInt = #line) { XCTAssertEqual( relativeExpectedPaths.absolutePathsStandardized().sorted(), - actualPaths.sorted(), + actualPaths.map(\.path).absolutePathsStandardized().sorted(), file: file, line: line ) diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index 2f763e9f0d..c9e027bb94 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -5,85 +5,86 @@ import XCTest @testable import SwiftLintFramework final class GlobTests: SwiftLintTestCase { - private var mockPath: String { - TestResources.path().appendingPathComponent("ProjectMock").filepath - } + private let mockPath = TestResources.path().appending(path: "ProjectMock", directoryHint: .isDirectory) func testNonExistingDirectory() { - XCTAssertTrue(Glob.resolveGlob("./bar/**").isEmpty) + XCTAssertTrue(Glob.resolveGlob("./bar/**".url()).isEmpty) } func testOnlyGlobForWildcard() { - let files = Glob.resolveGlob("foo/bar.swift") - XCTAssertEqual(files, ["foo/bar.swift"]) + let files = Glob.resolveGlob("foo/bar.swift".url()) + XCTAssertEqual(files, ["foo/bar.swift".url()]) } func testNoMatchReturnsEmpty() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("NoFile*.swift")) + let files = Glob.resolveGlob(mockPath.appending(path: "NoFile*.swift")) XCTAssertTrue(files.isEmpty) } func testMatchesFiles() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level*.swift")) - XCTAssertEqual(files, [mockPath.stringByAppendingPathComponent("Level0.swift")]) + let files = Glob.resolveGlob(mockPath.appending(path: "Level*.swift")) + XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) } func testMatchesSingleCharacter() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level?.swift")) - XCTAssertEqual(files, [mockPath.stringByAppendingPathComponent("Level0.swift")]) + let files = Glob.resolveGlob(mockPath.appending(path: "Level?.swift")) + XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) } func testMatchesOneCharacterInBracket() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[01].swift")) - XCTAssertEqual(files, [mockPath.stringByAppendingPathComponent("Level0.swift")]) + let files = Glob.resolveGlob(mockPath.appending(path: "Level[01].swift")) + XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) } func testNoMatchOneCharacterInBracket() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[ab].swift")) + let files = Glob.resolveGlob(mockPath.appending(path: "Level[ab].swift")) XCTAssertTrue(files.isEmpty) } func testMatchesCharacterInRange() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[0-9].swift")) - XCTAssertEqual(files, [mockPath.stringByAppendingPathComponent("Level0.swift")]) + let files = Glob.resolveGlob(mockPath.appending(path: "Level[0-9].swift")) + XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) } func testNoMatchCharactersInRange() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[a-z].swift")) + let files = Glob.resolveGlob(mockPath.appending(path: "Level[a-z].swift")) XCTAssertTrue(files.isEmpty) } func testMatchesMultipleFiles() { - let expectedFiles: Set = [ - mockPath.stringByAppendingPathComponent("Level0.swift"), - mockPath.stringByAppendingPathComponent("Directory.swift"), + let expectedFiles = [ + mockPath.appending(path: "Level0.swift"), + mockPath.appending(path: "Directory.swift/"), ] - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("*.swift")) - XCTAssertEqual(files.sorted(), expectedFiles.sorted()) + let files = Glob.resolveGlob(mockPath.appending(path: "*.swift")) + AssertEqualInAnyOder(files, expectedFiles) } func testMatchesNestedDirectory() { - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level1/*.swift")) - XCTAssertEqual(files, [mockPath.stringByAppendingPathComponent("Level1/Level1.swift")]) + let files = Glob.resolveGlob(mockPath.appending(path: "Level1/*.swift")) + XCTAssertEqual(files, [mockPath.appending(path: "Level1/Level1.swift")]) } func testGlobstarSupport() { - let expectedFiles = Set( - [ - "Directory.swift", - "Directory.swift/DirectoryLevel1.swift", - "Level0.swift", - "Level1/Level1.swift", - "Level1/Level2/Level2.swift", - "Level1/Level2/Level3/Level3.swift", - "NestedConfig/Test/Main.swift", - "NestedConfig/Test/Sub/Sub.swift", - ].map(mockPath.stringByAppendingPathComponent) - ) - - let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("**/*.swift")) - XCTAssertEqual(files.sorted(), expectedFiles.sorted()) + if #unavailable(macOS 26) { + // Older versions have double slashes in the returned paths. + return + } + + let expectedFiles = [ + "Directory.swift/", + "Directory.swift/DirectoryLevel1.swift", + "Level0.swift", + "Level1/Level1.swift", + "Level1/Level2/Level2.swift", + "Level1/Level2/Level3/Level3.swift", + "NestedConfig/Test/Main.swift", + "NestedConfig/Test/Sub/Sub.swift", + ].map { mockPath.appending(path: $0) } + + let files = Glob.resolveGlob(mockPath.appending(path: "**/*.swift")) + AssertEqualInAnyOder(files, expectedFiles) } func testCreateFilenameMatchers() { @@ -112,4 +113,12 @@ final class GlobTests: SwiftLintTestCase { assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/c/MyTest2.swift") assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/MyTests/c.swift") } + + // swiftlint:disable:next identifier_name + private func AssertEqualInAnyOder(_ lhs: [URL], _ rhs: [URL], file: StaticString = #filePath, line: UInt = #line) { + func compare(lhs: URL, rhs: URL) -> Bool { + lhs.path < rhs.path + } + XCTAssertEqual(lhs.sorted(by: compare), rhs.sorted(by: compare), file: file, line: line) + } } diff --git a/Tests/FileSystemAccessTests/ConfigurationTests+MultipleConfigs.swift b/Tests/FileSystemAccessTests/MultipleConfigurationsTests.swift similarity index 93% rename from Tests/FileSystemAccessTests/ConfigurationTests+MultipleConfigs.swift rename to Tests/FileSystemAccessTests/MultipleConfigurationsTests.swift index 937647187b..7272d5ec27 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests+MultipleConfigs.swift +++ b/Tests/FileSystemAccessTests/MultipleConfigurationsTests.swift @@ -13,7 +13,22 @@ private extension Configuration { } // swiftlint:disable:next type_body_length -extension ConfigurationTests { +final class MultipleConfigurationsTests: SwiftLintTestCase { + // MARK: Setup & Teardown + private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + Configuration.resetCache() + previousWorkingDir = FileManager.default.currentDirectoryPath + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) + } + + override func tearDown() { + super.tearDown() + XCTAssert(FileManager.default.changeCurrentDirectoryPath(previousWorkingDir)) + } + // MARK: - Rules Merging func testMerge() { let config0Merge2 = Mock.Config._0.merged(withChild: Mock.Config._2) @@ -102,7 +117,7 @@ extension ConfigurationTests { func testCustomRulesMerging() { let mergedConfiguration = Mock.Config._0CustomRules.merged( withChild: Mock.Config._2CustomRules, - rootDirectory: "" + rootDirectory: URL.cwd ) guard let mergedCustomRules = mergedConfiguration.rules.customRules else { @@ -120,7 +135,7 @@ extension ConfigurationTests { func testMergingAllowsDisablingParentsCustomRules() { let mergedConfiguration = Mock.Config._0CustomRules.merged( withChild: Mock.Config._2CustomRulesDisabled, - rootDirectory: "" + rootDirectory: URL.cwd ) guard let mergedCustomRules = mergedConfiguration.rules.customRules else { @@ -141,7 +156,7 @@ extension ConfigurationTests { // => all custom rules should be considered let mergedConfiguration = Mock.Config._0CustomRulesOnly.merged( withChild: Mock.Config._2CustomRules, - rootDirectory: "" + rootDirectory: URL.cwd ) guard let mergedCustomRules = mergedConfiguration.rules.customRules else { @@ -163,7 +178,7 @@ extension ConfigurationTests { // (because custom rules from base configuration would require explicit mention as one of the `only_rules`) let mergedConfiguration = Mock.Config._0CustomRulesOnly.merged( withChild: Mock.Config._2CustomRulesOnly, - rootDirectory: "" + rootDirectory: URL.cwd ) guard let mergedCustomRules = mergedConfiguration.rules.customRules else { @@ -182,7 +197,7 @@ extension ConfigurationTests { // Custom Rule severity gets reconfigured to "error" let mergedConfiguration = Mock.Config._0CustomRulesOnly.merged( withChild: Mock.Config._2CustomRulesReconfig, - rootDirectory: "" + rootDirectory: URL.cwd ) guard let mergedCustomRules = mergedConfiguration.rules.customRules else { @@ -259,22 +274,22 @@ extension ConfigurationTests { } for path in [Mock.Dir.childConfigTest1, Mock.Dir.childConfigTest2] { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path.filepath)) assertEqualExceptForFileGraph( - Configuration(configurationFiles: ["main.yml"]), - Configuration(configurationFiles: ["expected.yml"]) + Configuration(configurationFiles: ["main.yml".url()]), + Configuration(configurationFiles: ["expected.yml".url()]) ) } } func testValidParentConfig() { for path in [Mock.Dir.parentConfigTest1, Mock.Dir.parentConfigTest2] { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path.filepath)) assertEqualExceptForFileGraph( - Configuration(configurationFiles: ["main.yml"]), - Configuration(configurationFiles: ["expected.yml"]) + Configuration(configurationFiles: ["main.yml".url()]), + Configuration(configurationFiles: ["expected.yml".url()]) ) } } @@ -285,13 +300,17 @@ extension ConfigurationTests { } for path in [Mock.Dir.childConfigTest1, Mock.Dir.childConfigTest2] { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path.filepath)) assertEqualExceptForFileGraph( Configuration( - configurationFiles: ["main.yml", "child1.yml", "child2.yml"] + configurationFiles: [ + "main.yml".url(), + "child1.yml".url(), + "child2.yml".url(), + ] ), - Configuration(configurationFiles: ["expected.yml"]) + Configuration(configurationFiles: ["expected.yml".url()]) ) } } @@ -305,7 +324,7 @@ extension ConfigurationTests { Mock.Dir.parentConfigCycle2, Mock.Dir.parentConfigCycle3, ] { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path.filepath)) // If the cycle is properly detected, the config should equal the default config. XCTAssertEqual( @@ -316,12 +335,12 @@ extension ConfigurationTests { } func testCommandLineConfigsCycleDetection() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.childConfigCycle4)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.childConfigCycle4.filepath)) // If the cycle is properly detected, the config should equal the default config. assertEqualExceptForFileGraph( Configuration( - configurationFiles: ["main.yml", "child.yml"], + configurationFiles: ["main.yml".url(), "child.yml".url()], useDefaultConfigOnFailure: true ), Configuration() @@ -547,11 +566,11 @@ extension ConfigurationTests { // MARK: - Remote Configs func testValidRemoteChildConfig() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigChild)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigChild.filepath)) assertEqualExceptForFileGraph( Configuration( - configurationFiles: ["main.yml"], + configurationFiles: ["main.yml".url()], mockedNetworkResults: [ "https://www.mock.com": """ @@ -561,16 +580,16 @@ extension ConfigurationTests { """, ] ), - Configuration(configurationFiles: ["expected.yml"]) + Configuration(configurationFiles: ["expected.yml".url()]) ) } func testValidRemoteParentConfig() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigParent)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigParent.filepath)) assertEqualExceptForFileGraph( Configuration( - configurationFiles: ["main.yml"], + configurationFiles: ["main.yml".url()], mockedNetworkResults: [ "https://www.mock.com": """ @@ -586,12 +605,12 @@ extension ConfigurationTests { """, ] ), - Configuration(configurationFiles: ["expected.yml"]) + Configuration(configurationFiles: ["expected.yml".url()]) ) } func testsRemoteConfigNotAllowedToReferenceLocalConfig() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigLocalRef)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigLocalRef.filepath)) // If the remote file is not allowed to reference a local file, the config should equal the default config. XCTAssertEqual( @@ -611,7 +630,7 @@ extension ConfigurationTests { } func testRemoteConfigCycleDetection() { - XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigCycle)) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigCycle.filepath)) // If the cycle is properly detected, the config should equal the default config. XCTAssertEqual( @@ -651,9 +670,15 @@ extension ConfigurationTests { }) ) - XCTAssertEqual(Set(configuration1.includedPaths), Set(configuration2.includedPaths)) + XCTAssertEqual( + configuration1.includedPaths.map(\.path).sorted(), + configuration2.includedPaths.map(\.path).sorted() + ) - XCTAssertEqual(Set(configuration1.excludedPaths), Set(configuration2.excludedPaths)) + XCTAssertEqual( + configuration1.excludedPaths.map(\.path).sorted(), + configuration2.excludedPaths.map(\.path).sorted() + ) } } diff --git a/Tests/FileSystemAccessTests/ReporterTests.swift b/Tests/FileSystemAccessTests/ReporterTests.swift index 85bd0090a8..71067c6531 100644 --- a/Tests/FileSystemAccessTests/ReporterTests.swift +++ b/Tests/FileSystemAccessTests/ReporterTests.swift @@ -10,23 +10,22 @@ final class ReporterTests: SwiftLintTestCase { private let violations = [ StyleViolation( ruleDescription: LineLengthRule.description, - location: Location(file: "filename", line: 1, character: 1), + location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 1), reason: "Violation Reason 1" ), StyleViolation( ruleDescription: LineLengthRule.description, severity: .error, - location: Location(file: "filename", line: 1), + location: Location(file: URL.cwd.appending(path: "filename"), line: 1), reason: "Violation Reason 2" ), StyleViolation( ruleDescription: SyntacticSugarRule.description, severity: .error, location: Location( - file: URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent("path") - .appendingPathComponent("file.swift") - .path, + file: URL.cwd + .appending(path: "path", directoryHint: .isDirectory) + .appending(path: "file.swift", directoryHint: .notDirectory), line: 1, character: 2 ), @@ -46,8 +45,8 @@ final class ReporterTests: SwiftLintTestCase { } private func stringFromFile(_ filename: String) -> String { - let path = TestResources.path().appendingPathComponent(filename) - return SwiftLintFile(path: path.filepath)!.contents + let path = TestResources.path().appending(path: filename, directoryHint: .notDirectory) + return SwiftLintFile(path: path)!.contents } func testXcodeReporter() throws { @@ -198,15 +197,14 @@ final class ReporterTests: SwiftLintTestCase { } func testRelativePathReporterPaths() { - let relativePath = "filename" - let absolutePath = FileManager.default.currentDirectoryPath + "/" + relativePath - let location = Location(file: absolutePath, line: 1, character: 2) + let relativePath = "filename".url() + let location = Location(file: relativePath, line: 1, character: 2) let violation = StyleViolation(ruleDescription: LineLengthRule.description, location: location, reason: "Violation Reason") let result = RelativePathReporter.generateReport([violation]) - XCTAssertFalse(result.contains(absolutePath)) - XCTAssertTrue(result.contains(relativePath)) + XCTAssertFalse(result.contains(relativePath.filepath)) + XCTAssertTrue(result.contains(relativePath.relativeFilepath)) } func testSummaryReporter() { @@ -214,7 +212,7 @@ final class ReporterTests: SwiftLintTestCase { .trimmingTrailingCharacters(in: .whitespacesAndNewlines) let correctableViolation = StyleViolation( ruleDescription: VerticalWhitespaceOpeningBracesRule.description, - location: Location(file: "filename", line: 1, character: 2), + location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 2), reason: "Violation Reason" ) let result = SummaryReporter.generateReport(violations + [correctableViolation]) @@ -236,11 +234,9 @@ final class ReporterTests: SwiftLintTestCase { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short - let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).path - let reference = stringFromFile(referenceFile).replacingOccurrences( of: "${CURRENT_WORKING_DIRECTORY}", - with: cwd + with: URL.cwd.path ).replacingOccurrences( of: "${SWIFTLINT_VERSION}", with: SwiftLintFramework.Version.current.value @@ -252,9 +248,9 @@ final class ReporterTests: SwiftLintTestCase { let convertedReference = try stringConverter(reference) let convertedReporterOutput = try stringConverter(reporterOutput) if convertedReference != convertedReporterOutput { - let referenceURL = TestResources.path().appendingPathComponent(referenceFile) + let referenceURL = TestResources.path().appending(path: referenceFile, directoryHint: .notDirectory) try reporterOutput.replacingOccurrences( - of: cwd, + of: URL.cwd.path, with: "${CURRENT_WORKING_DIRECTORY}" ).replacingOccurrences( of: SwiftLintFramework.Version.current.value, diff --git a/Tests/FileSystemAccessTests/Resources/CannedCSVReporterOutput.csv b/Tests/FileSystemAccessTests/Resources/CannedCSVReporterOutput.csv index 2db0eb122f..0fd0b4363e 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedCSVReporterOutput.csv +++ b/Tests/FileSystemAccessTests/Resources/CannedCSVReporterOutput.csv @@ -1,5 +1,5 @@ file,line,character,severity,type,reason,rule_id -filename,1,1,Warning,Line Length,Violation Reason 1,line_length -filename,1,,Error,Line Length,Violation Reason 2,line_length +${CURRENT_WORKING_DIRECTORY}/filename,1,1,Warning,Line Length,Violation Reason 1,line_length +${CURRENT_WORKING_DIRECTORY}/filename,1,,Error,Line Length,Violation Reason 2,line_length ${CURRENT_WORKING_DIRECTORY}/path/file.swift,1,2,Error,Syntactic Sugar,"Shorthand syntactic sugar should be used, i.e. [Int] instead of Array",syntactic_sugar ,,,Error,Colon Spacing,Colons should be next to the identifier when specifying a type and next to the key in dictionary literals,colon \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/CannedCheckstyleReporterOutput.xml b/Tests/FileSystemAccessTests/Resources/CannedCheckstyleReporterOutput.xml index 320a251ce8..11705bc82d 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedCheckstyleReporterOutput.xml +++ b/Tests/FileSystemAccessTests/Resources/CannedCheckstyleReporterOutput.xml @@ -3,11 +3,11 @@ - - - - + + + + \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/CannedCodeClimateReporterOutput.json b/Tests/FileSystemAccessTests/Resources/CannedCodeClimateReporterOutput.json index c6f197c017..a3dc2eede7 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedCodeClimateReporterOutput.json +++ b/Tests/FileSystemAccessTests/Resources/CannedCodeClimateReporterOutput.json @@ -3,7 +3,7 @@ "check_name" : "Line Length", "description" : "Violation Reason 1", "engine_name" : "SwiftLint", - "fingerprint" : "4a17aef14fdc2dbdd95ab2ee78d1b7d6cc289539d290b283cbabedd30e929f5f", + "fingerprint" : "19ad62b3ad78be4d03fc82c2846bef0f873c4235611321aaca16ee36f72214f9", "location" : { "lines" : { "begin" : 1, @@ -18,7 +18,7 @@ "check_name" : "Line Length", "description" : "Violation Reason 2", "engine_name" : "SwiftLint", - "fingerprint" : "4a17aef14fdc2dbdd95ab2ee78d1b7d6cc289539d290b283cbabedd30e929f5f", + "fingerprint" : "f7186279d6dc9e8bcd0a13adbc07e111e1bc95178953d0c5d061534c75a81e2f", "location" : { "lines" : { "begin" : 1, @@ -33,7 +33,7 @@ "check_name" : "Syntactic Sugar", "description" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", "engine_name" : "SwiftLint", - "fingerprint" : "752322cea7c7ad97a20777d51a8d44c33a7e037290344c8fed6881ec916b6f1a", + "fingerprint" : "740b45a83d152b64836c6703474f2dc8d5f6c89b792d04f83cd058a92fa5db46", "location" : { "lines" : { "begin" : 1, @@ -48,7 +48,7 @@ "check_name" : "Colon Spacing", "description" : "Colons should be next to the identifier when specifying a type and next to the key in dictionary literals", "engine_name" : "SwiftLint", - "fingerprint" : "9b1ddedc847d23a54124cb02a56452fc01c23df8f3babc07a6a68cf2449b14a6", + "fingerprint" : "25477e0cb480fc03bfdcbfa0d7cf569515a6e6ac372406e4121213585404cd9c", "location" : { "lines" : { "begin" : null, diff --git a/Tests/FileSystemAccessTests/Resources/CannedEmojiReporterOutput.txt b/Tests/FileSystemAccessTests/Resources/CannedEmojiReporterOutput.txt index 7830e72d6c..5040173263 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedEmojiReporterOutput.txt +++ b/Tests/FileSystemAccessTests/Resources/CannedEmojiReporterOutput.txt @@ -1,7 +1,7 @@ +${CURRENT_WORKING_DIRECTORY}/filename +⛔️ Line 1: Violation Reason 2 (line_length) +⚠️ Line 1: Violation Reason 1 (line_length) ${CURRENT_WORKING_DIRECTORY}/path/file.swift ⛔️ Line 1: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) Other -⛔️ Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) -filename -⛔️ Line 1: Violation Reason 2 (line_length) -⚠️ Line 1: Violation Reason 1 (line_length) \ No newline at end of file +⛔️ Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/CannedJSONReporterOutput.json b/Tests/FileSystemAccessTests/Resources/CannedJSONReporterOutput.json index a125107eb3..d67f3d5332 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedJSONReporterOutput.json +++ b/Tests/FileSystemAccessTests/Resources/CannedJSONReporterOutput.json @@ -1,7 +1,7 @@ [ { "character" : 1, - "file" : "filename", + "file" : "${CURRENT_WORKING_DIRECTORY}/filename", "line" : 1, "reason" : "Violation Reason 1", "rule_id" : "line_length", @@ -10,7 +10,7 @@ }, { "character" : null, - "file" : "filename", + "file" : "${CURRENT_WORKING_DIRECTORY}/filename", "line" : 1, "reason" : "Violation Reason 2", "rule_id" : "line_length", @@ -35,4 +35,4 @@ "severity" : "Error", "type" : "Colon Spacing" } -] +] \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/CannedJunitReporterOutput.xml b/Tests/FileSystemAccessTests/Resources/CannedJunitReporterOutput.xml index db64bdf072..3be34c3d59 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedJunitReporterOutput.xml +++ b/Tests/FileSystemAccessTests/Resources/CannedJunitReporterOutput.xml @@ -1,10 +1,10 @@ - + Warning:Line:1 - + Error:Line:1 diff --git a/Tests/FileSystemAccessTests/Resources/CannedMarkdownReporterOutput.md b/Tests/FileSystemAccessTests/Resources/CannedMarkdownReporterOutput.md index d74bd35e7f..0e734a8449 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedMarkdownReporterOutput.md +++ b/Tests/FileSystemAccessTests/Resources/CannedMarkdownReporterOutput.md @@ -1,6 +1,6 @@ file | line | severity | reason | rule_id --- | --- | --- | --- | --- -filename | 1 | :warning: | Line Length: Violation Reason 1 | line_length -filename | 1 | :stop\_sign: | Line Length: Violation Reason 2 | line_length +${CURRENT_WORKING_DIRECTORY}/filename | 1 | :warning: | Line Length: Violation Reason 1 | line_length +${CURRENT_WORKING_DIRECTORY}/filename | 1 | :stop\_sign: | Line Length: Violation Reason 2 | line_length ${CURRENT_WORKING_DIRECTORY}/path/file.swift | 1 | :stop\_sign: | Syntactic Sugar: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array | syntactic_sugar | | :stop\_sign: | Colon Spacing: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals | colon \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/CannedXcodeReporterOutput.txt b/Tests/FileSystemAccessTests/Resources/CannedXcodeReporterOutput.txt index 553120fb05..dbd7a60b3f 100644 --- a/Tests/FileSystemAccessTests/Resources/CannedXcodeReporterOutput.txt +++ b/Tests/FileSystemAccessTests/Resources/CannedXcodeReporterOutput.txt @@ -1,4 +1,4 @@ -filename:1:1: warning: Line Length Violation: Violation Reason 1 (line_length) -filename:1:1: error: Line Length Violation: Violation Reason 2 (line_length) +${CURRENT_WORKING_DIRECTORY}/filename:1:1: warning: Line Length Violation: Violation Reason 1 (line_length) +${CURRENT_WORKING_DIRECTORY}/filename:1:1: error: Line Length Violation: Violation Reason 2 (line_length) ${CURRENT_WORKING_DIRECTORY}/path/file.swift:1:2: error: Syntactic Sugar Violation: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) :1:1: error: Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) \ No newline at end of file diff --git a/Tests/FileSystemAccessTests/Resources/ProjectMock/Baseline.json b/Tests/FileSystemAccessTests/Resources/ProjectMock/Baseline.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Tests/FileSystemAccessTests/Resources/ProjectMock/Baseline.json @@ -0,0 +1 @@ +{} diff --git a/Tests/FileSystemAccessTests/SourceKitCrashTests.swift b/Tests/FileSystemAccessTests/SourceKitCrashTests.swift index db6fbfdd54..4f6b26875d 100644 --- a/Tests/FileSystemAccessTests/SourceKitCrashTests.swift +++ b/Tests/FileSystemAccessTests/SourceKitCrashTests.swift @@ -35,7 +35,10 @@ final class SourceKitCrashTests: SwiftLintTestCase { } func testRulesWithFileThatCrashedSourceKitService() throws { - let file = try XCTUnwrap(SwiftLintFile(path: "\(TestResources.path().filepath)/ProjectMock/Level0.swift")) + let path = TestResources.path() + .appending(path: "ProjectMock", directoryHint: .isDirectory) + .appending(path: "Level0.swift", directoryHint: .notDirectory) + let file = try XCTUnwrap(SwiftLintFile(path: path)) file.sourcekitdFailed = true file.assertHandler = { XCTFail("If this called, rule's SourceKitFreeRule is not properly configured") diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 729f9c8609..fc508e221d 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -9,7 +9,9 @@ import XCTest final class CustomRulesTests: SwiftLintTestCase { private typealias Configuration = RegexConfiguration - private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path().filepath)/test.txt")! } + private var testFile: SwiftLintFile { + SwiftLintFile(path: TestResources.path().appending(path: "test.txt", directoryHint: .notDirectory))! + } override func invokeTest() { CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { @@ -870,9 +872,9 @@ final class CustomRulesTests: SwiftLintTestCase { regexConfig.included = [try RegularExpression(pattern: "\\.swift$")] regexConfig.excluded = [try RegularExpression(pattern: "Tests")] - XCTAssertTrue(regexConfig.shouldValidate(filePath: "/path/to/file.swift")) - XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/file.m")) - XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/Tests/file.swift")) + XCTAssertTrue(regexConfig.shouldValidate(filePath: "/path/to/file.swift".url())) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/file.m".url())) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/Tests/file.swift".url())) } // MARK: - only_rules support diff --git a/Tests/FrameworkTests/LinterCacheTests.swift b/Tests/FrameworkTests/LinterCacheTests.swift index 3168bbe570..abf70f2210 100644 --- a/Tests/FrameworkTests/LinterCacheTests.swift +++ b/Tests/FrameworkTests/LinterCacheTests.swift @@ -22,7 +22,7 @@ private struct CacheTestHelper { self.cache = cache } - fileprivate func makeViolations(file: String) -> [StyleViolation] { + fileprivate func makeViolations(file: URL) -> [StyleViolation] { touch(file: file) return [ StyleViolation(ruleDescription: ruleDescription, @@ -40,11 +40,11 @@ private struct CacheTestHelper { try! Configuration(dict: dict, ruleList: ruleList) // swiftlint:disable:this force_try } - fileprivate func touch(file: String) { + fileprivate func touch(file: URL) { fileManager.stubbedModificationDateByPath[file] = Date() } - fileprivate func remove(file: String) { + fileprivate func remove(file: URL) { fileManager.stubbedModificationDateByPath[file] = nil } @@ -54,15 +54,14 @@ private struct CacheTestHelper { } private class TestFileManager: LintableFileManager { - fileprivate func filesToLint(inPath _: String, - rootDirectory _: String? = nil, - excluder _: Excluder) -> [String] { + fileprivate func filesToLint(inPath _: URL, + excluder _: Excluder) -> [URL] { [] } - fileprivate var stubbedModificationDateByPath = [String: Date]() + fileprivate var stubbedModificationDateByPath = [URL: Date]() - fileprivate func modificationDate(forFileAtPath path: String) -> Date? { + fileprivate func modificationDate(forFileAtPath path: URL) -> Date? { stubbedModificationDateByPath[path] } } @@ -77,7 +76,7 @@ final class LinterCacheTests: SwiftLintTestCase { } private func cacheAndValidate(violations: [StyleViolation], - forFile: String, + forFile: URL, configuration: Configuration, file: StaticString = #filePath, line: UInt = #line) { @@ -90,7 +89,7 @@ final class LinterCacheTests: SwiftLintTestCase { private func cacheAndValidateNoViolationsTwoFiles(configuration: Configuration, file: StaticString = #filePath, line: UInt = #line) { - let (file1, file2) = ("file1.swift", "file2.swift") + let (file1, file2) = ("file1.swift".url(), "file2.swift".url()) // swiftlint:disable:next force_cast let fileManager = cache.fileManager as! TestFileManager fileManager.stubbedModificationDateByPath = [file1: Date(), file2: Date()] @@ -104,7 +103,7 @@ final class LinterCacheTests: SwiftLintTestCase { file: StaticString = #filePath, line: UInt = #line) throws { let newConfig = try Configuration(dict: dict) - let (file1, file2) = ("file1.swift", "file2.swift") + let (file1, file2) = ("file1.swift".url(), "file2.swift".url()) XCTAssertNil(cache.violations(forFile: file1, configuration: newConfig), file: (file), line: line) XCTAssertNil(cache.violations(forFile: file2, configuration: newConfig), file: (file), line: line) @@ -118,7 +117,7 @@ final class LinterCacheTests: SwiftLintTestCase { // Two subsequent lints with no changes reuses cache func testUnchangedFilesReusesCache() { let helper = makeCacheTestHelper(dict: ["only_rules": ["mock"]]) - let file = "foo.swift" + let file = "foo.swift".url() let violations = helper.makeViolations(file: file) cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration) @@ -129,7 +128,7 @@ final class LinterCacheTests: SwiftLintTestCase { func testConfigFileReorderedReusesCache() { let helper = makeCacheTestHelper(dict: ["only_rules": ["mock"], "disabled_rules": [Any]()]) - let file = "foo.swift" + let file = "foo.swift".url() let violations = helper.makeViolations(file: file) cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration) @@ -139,7 +138,7 @@ final class LinterCacheTests: SwiftLintTestCase { func testConfigFileWhitespaceAndCommentsChangedOrAddedOrRemovedReusesCache() throws { let helper = makeCacheTestHelper(dict: try YamlParser.parse("only_rules:\n - mock")) - let file = "foo.swift" + let file = "foo.swift".url() let violations = helper.makeViolations(file: file) cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration) @@ -153,7 +152,7 @@ final class LinterCacheTests: SwiftLintTestCase { func testConfigFileUnrelatedKeysChangedOrAddedOrRemovedReusesCache() { let helper = makeCacheTestHelper(dict: ["only_rules": ["mock"], "reporter": "json"]) - let file = "foo.swift" + let file = "foo.swift".url() let violations = helper.makeViolations(file: file) cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration) @@ -169,7 +168,7 @@ final class LinterCacheTests: SwiftLintTestCase { // file to be re-linted, with the cache used for all other files func testChangedFileCausesJustThatFileToBeLintWithCacheUsedForAllOthers() { let helper = makeCacheTestHelper(dict: ["only_rules": ["mock"], "reporter": "json"]) - let (file1, file2) = ("file1.swift", "file2.swift") + let (file1, file2) = ("file1.swift".url(), "file2.swift".url()) let violations1 = helper.makeViolations(file: file1) let violations2 = helper.makeViolations(file: file2) @@ -182,7 +181,7 @@ final class LinterCacheTests: SwiftLintTestCase { func testFileRemovedPreservesThatFileInTheCacheAndDoesntCauseAnyOtherFilesToBeLinted() { let helper = makeCacheTestHelper(dict: ["only_rules": ["mock"], "reporter": "json"]) - let (file1, file2) = ("file1.swift", "file2.swift") + let (file1, file2) = ("file1.swift".url(), "file2.swift".url()) let violations1 = helper.makeViolations(file: file1) let violations2 = helper.makeViolations(file: file2) @@ -295,7 +294,7 @@ final class LinterCacheTests: SwiftLintTestCase { let fileManager = TestFileManager() cache = LinterCache(fileManager: fileManager) let helper = makeCacheTestHelper(dict: [:]) - let file = "foo.swift" + let file = "foo.swift".url() let violations = helper.makeViolations(file: file) cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration) diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 8c8819e49b..8065c67260 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -7,28 +7,28 @@ import XCTest final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { private func fixturePath(_ scenario: String) -> URL { - URL(fileURLWithPath: #filePath) + #filePath.url(directoryHint: .isDirectory) .deletingLastPathComponent() - .appending(path: "Resources") - .appending(component: scenario) + .appending(path: "Resources", directoryHint: .isDirectory) + .appending(path: scenario, directoryHint: .isDirectory) } /// Returns the paths of lintable files relative to the fixture directory. - private func lintableFilePaths(in scenario: String, configFile: String? = nil, inPath: String = ".") -> [String] { - let scenarioPath = fixturePath(scenario).filepath + private func lintableFilePaths(in scenario: String, configFile: String? = nil, inPath: String? = nil) -> [String] { + let scenarioPath = fixturePath(scenario) let previousDir = FileManager.default.currentDirectoryPath - XCTAssert(FileManager.default.changeCurrentDirectoryPath(scenarioPath)) defer { _ = FileManager.default.changeCurrentDirectoryPath(previousDir) } + XCTAssert(FileManager.default.changeCurrentDirectoryPath(scenarioPath.filepath)) - let config = Configuration(configurationFiles: configFile.map { [$0] } ?? []) + let config = Configuration(configurationFiles: configFile.map { [$0.url()] } ?? []) let files = config.lintableFiles( - inPath: inPath, + inPath: inPath.map { $0.url() } ?? URL.cwd, forceExclude: false, excludeByPrefix: false ) - return files.map { $0.path!.path(relativeTo: scenarioPath) }.sorted() + return files.map { $0.path!.relativeFilepath }.sorted() } func testParentChildSameDirectory() { @@ -135,10 +135,10 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { let config = Configuration(configurationFiles: []) let moduleAFile = SwiftLintFile( - path: scenarioPath.appending(path: "ModuleA/File.swift").filepath + path: scenarioPath.appending(path: "ModuleA/File.swift") )! let moduleBFile = SwiftLintFile( - path: scenarioPath.appending(path: "ModuleB/File.swift").filepath + path: scenarioPath.appending(path: "ModuleB/File.swift") )! XCTAssertTrue( @@ -158,11 +158,11 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { let scenarioPath = fixturePath("_4_nested_basic") let moduleAFile = SwiftLintFile( - path: scenarioPath.appending(path: "ModuleB/File.swift").filepath + path: scenarioPath.appending(path: "ModuleB/File.swift") )! XCTAssertFalse( - Configuration(configurationFiles: [scenarioPath.appending(path: "root.yml").filepath]) + Configuration(configurationFiles: [scenarioPath.appending(path: "root.yml")]) .configuration(for: moduleAFile) .rules .map { type(of: $0).identifier } diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index e753d96fb4..f46a07dd66 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -12,7 +12,7 @@ private let config: Configuration = { .deletingLastPathComponent.bridge() .deletingLastPathComponent _ = FileManager.default.changeCurrentDirectoryPath(rootProjectDirectory) - return Configuration(configurationFiles: [Configuration.defaultFileName]) + return Configuration(configurationFiles: [Configuration.defaultFileName.url()]) }() final class IntegrationTests: SwiftLintTestCase { @@ -23,11 +23,11 @@ final class IntegrationTests: SwiftLintTestCase { ) // This is as close as we're ever going to get to a self-hosting linter. let swiftFiles = config.lintableFiles( - inPath: "", + inPath: URL.cwd, forceExclude: false, excludeByPrefix: false) XCTAssert( - swiftFiles.contains(where: { #filePath.bridge().absolutePathRepresentation() == $0.path }), + swiftFiles.contains(where: { $0.path?.filepath.hasSuffix(#filePath) == true }), "current file should be included" ) @@ -36,7 +36,7 @@ final class IntegrationTests: SwiftLintTestCase { Linter(file: $0, configuration: config).collect(into: storage).styleViolations(using: storage) } violations.forEach { violation in - violation.location.file!.withStaticString { + violation.location.file!.relativePath.withStaticString { XCTFail(violation.reason, file: $0, line: UInt(violation.location.line!)) } } @@ -48,7 +48,7 @@ final class IntegrationTests: SwiftLintTestCase { "Corrections are not verified in CI" ) let swiftFiles = config.lintableFiles( - inPath: "", + inPath: URL.cwd, forceExclude: false, excludeByPrefix: false) let storage = RuleStorage() diff --git a/Tests/TestHelpers/TestHelpers.swift b/Tests/TestHelpers/TestHelpers.swift index 4f6432bee8..af9d8d6354 100644 --- a/Tests/TestHelpers/TestHelpers.swift +++ b/Tests/TestHelpers/TestHelpers.swift @@ -31,11 +31,11 @@ extension PlatformInfo: Decodable { } private let info: PlatformInfo = { - let sdk = URL(fileURLWithPath: sdkPath(), isDirectory: true) + let sdk = sdkPath().url(directoryHint: .isDirectory) .deletingLastPathComponent() .deletingLastPathComponent() .deletingLastPathComponent() - .appendingPathComponent("Info.plist") + .appending(path: "Info.plist", directoryHint: .notDirectory) guard let data = try? Data(contentsOf: sdk), let info = try? PropertyListDecoder().decode(PlatformInfo.self, from: data) else { fatalError("invalid platform SDK - couldn't decode \(sdk.path)") @@ -52,41 +52,41 @@ private let violationMarkerChar = violationMarker.first! private extension SwiftLintFile { static func testFile(withContents contents: String, persistToDisk: Bool = false) -> SwiftLintFile { if persistToDisk { - let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - .appendingPathComponent(UUID().uuidString) + let url = URL.temporaryDirectory + .appending(path: UUID().uuidString, directoryHint: .notDirectory) .appendingPathExtension("swift") _ = try? contents.data(using: .utf8)!.write(to: url) - return SwiftLintFile(path: url.path, isTestFile: true)! + return SwiftLintFile(path: url, isTestFile: true)! } return SwiftLintFile(contents: contents, isTestFile: true) } func makeCompilerArguments() -> [String] { let sdk = sdkPath() - let frameworks = URL(fileURLWithPath: sdk, isDirectory: true) + let frameworks = sdk.url(directoryHint: .isDirectory) .deletingLastPathComponent() .deletingLastPathComponent() - .appendingPathComponent("Library") - .appendingPathComponent("Frameworks") - .path + .appending(path: "Library", directoryHint: .isDirectory) + .appending(path: "Frameworks", directoryHint: .isDirectory) + .filepath let arguments = [ "-F", frameworks, "-sdk", sdk, "-Xfrontend", "-enable-objc-interop", "-j4", - path!, + path!.filepath, ] #if os(Windows) - let XCTestPath = URL(fileURLWithPath: sdk, isDirectory: true) + let XCTestPath = sdk.url(directoryHint: .isDirectory) .deletingLastPathComponent() .deletingLastPathComponent() - .appendingPathComponent("Library") - .appendingPathComponent("XCTest-\(info.defaults.versionXCTest)") - .appendingPathComponent("usr") - .appendingPathComponent("lib") - .appendingPathComponent("swift") - .appendingPathComponent("windows") + .appending(path: "Library", directoryHint: .isDirectory) + .appending(path: "XCTest-\(info.defaults.versionXCTest)", directoryHint: .isDirectory) + .appending(path: "usr", directoryHint: .isDirectory) + .appending(path: "lib", directoryHint: .isDirectory) + .appending(path: "swift", directoryHint: .isDirectory) + .appending(path: "windows", directoryHint: .isDirectory) .path return ["-I", XCTestPath] + arguments #else @@ -95,12 +95,6 @@ private extension SwiftLintFile { } } -public extension String { - func stringByAppendingPathComponent(_ pathComponent: String) -> String { - URL(fileURLWithPath: self).appendingPathComponent(pathComponent).filepath - } -} - public let allRuleIdentifiers = Set(RuleRegistry.shared.list.list.keys) public extension Configuration { @@ -282,7 +276,7 @@ private extension Configuration { file: before.file, line: before.line) let path = file.path! do { - let corrected = try String(contentsOfFile: path, encoding: .utf8) + let corrected = try String(contentsOf: path, encoding: .utf8) XCTAssertEqual( corrected, expected.code, diff --git a/Tests/TestHelpers/TestResources.swift b/Tests/TestHelpers/TestResources.swift index d154988bf7..7ab70b650b 100644 --- a/Tests/TestHelpers/TestResources.swift +++ b/Tests/TestHelpers/TestResources.swift @@ -3,14 +3,15 @@ import SwiftLintCore public enum TestResources { public static func path(_ calleePath: String = #filePath) -> URL { - let folder = URL(fileURLWithPath: calleePath, isDirectory: false).deletingLastPathComponent() + let folder = calleePath.url(directoryHint: .notDirectory).deletingLastPathComponent() if let rootProjectDirectory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] { - return URL( - fileURLWithPath: "\(rootProjectDirectory)/Tests/\(folder.lastPathComponent)/Resources", - isDirectory: true) + return rootProjectDirectory.url(directoryHint: .isDirectory) + .appending(path: "Tests", directoryHint: .isDirectory) + .appending(path: folder.lastPathComponent, directoryHint: .isDirectory) + .appending(path: "Resources", directoryHint: .isDirectory) } return folder - .appendingPathComponent("Resources") + .appending(path: "Resources", directoryHint: .isDirectory) .resolvingSymlinksInPath() } } From 50680684ed6fcd27ab2cae5f2d7f889b3fa04d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 10 Jan 2026 20:10:23 +0100 Subject: [PATCH 03/15] Extend basic globbing on Windows --- Source/SwiftLintFramework/Helpers/Glob.swift | 110 +++++++++++++++---- Tests/FileSystemAccessTests/GlobTests.swift | 20 +++- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/Source/SwiftLintFramework/Helpers/Glob.swift b/Source/SwiftLintFramework/Helpers/Glob.swift index c41ccc761c..61b1282503 100644 --- a/Source/SwiftLintFramework/Helpers/Glob.swift +++ b/Source/SwiftLintFramework/Helpers/Glob.swift @@ -26,26 +26,7 @@ struct Glob { return expandGlobstar(pattern: pattern) .reduce(into: [URL]()) { paths, pattern in #if os(Windows) - pattern.withUnsafeFileSystemRepresentation { - var ffd = WIN32_FIND_DATAW() - - let hDirectory: HANDLE = String(cString: $0!).withCString(encodedAs: UTF16.self) { - FindFirstFileW($0, &ffd) - } - if hDirectory == INVALID_HANDLE_VALUE { return } - defer { FindClose(hDirectory) } - - repeat { - let path: String = withUnsafePointer(to: &ffd.cFileName) { - $0.withMemoryRebound(to: UInt16.self, - capacity: MemoryLayout.size(ofValue: $0) / MemoryLayout.size) { - String(decodingCString: $0, as: UTF16.self) - } - } - if path == "." || path == ".." { continue } - paths.append(path.url()) - } while FindNextFileW(hDirectory, &ffd) - } + paths.append(contentsOf: Self.windowsResolve(pattern)) #else var globResult = glob_t() defer { globfree(&globResult) } @@ -60,16 +41,99 @@ struct Glob { } #endif } - .unique } + #if os(Windows) + private static func windowsResolve(_ pattern: URL) -> [URL] { + let wildcardSet = CharacterSet(charactersIn: "*?[]") + + let native = pattern.filepath + guard native.rangeOfCharacter(from: wildcardSet) != nil else { + return [pattern] + } + + // Find base directory before the first wildcard in the native path + var baseDirPath: String + var remainder: String + if let firstWCIndex = native.firstIndex(where: { "*?[]".contains($0) }) { + let upToWildcard = native[..) -> [URL] { + guard let first = segments.first else { + return [base] + } + + let wildcardSet = CharacterSet(charactersIn: "*?[]") + let isLast = segments.count == 1 + if first.rangeOfCharacter(from: wildcardSet) == nil { + let nextBase = base.appending(path: first, directoryHint: .isDirectory) + return windowsExpand(base: nextBase, segments: segments.dropFirst()) + } + + // Segment contains wildcard -> enumerate matches using FindFirstFileW + var results = [URL]() + let searchURL = base.appending(path: first) + searchURL.withUnsafeFileSystemRepresentation { cPath in + guard let cPath else { return } + var ffd = WIN32_FIND_DATAW() + let hFind: HANDLE = String(cString: cPath).withCString(encodedAs: UTF16.self) { + FindFirstFileW($0, &ffd) + } + if hFind == INVALID_HANDLE_VALUE { return } + defer { FindClose(hFind) } + + repeat { + let name: String = withUnsafePointer(to: &ffd.cFileName) { + $0.withMemoryRebound(to: UInt16.self, + capacity: MemoryLayout.size(ofValue: $0) / MemoryLayout.size) { + String(decodingCString: $0, as: UTF16.self) + } + } + if name == "." || name == ".." { continue } + let isDir = (ffd.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) != 0 + let matchedURL = base.appending(path: name, directoryHint: isDir ? .isDirectory : .inferFromPath) + + if isLast { + results.append(matchedURL) + } else if isDir { + let tail = segments.dropFirst() + results.append(contentsOf: windowsExpand(base: matchedURL, segments: tail)) + } + } while FindNextFileW(hFind, &ffd) + } + + return results + } + #endif + static func createFilenameMatchers(root: String, pattern: String) -> [FilenameMatcher] { var absolutPathPattern = pattern + #if os(Windows) + if !pattern.contains(":") { + // If the root is not already part of the pattern, prepend it. + absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern + } + #else if !pattern.starts(with: "/") { // If the root is not already part of the pattern, prepend it. absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern } - absolutPathPattern = absolutPathPattern.absolutePathStandardized() + #endif if pattern.hasSuffix(".swift") || pattern.hasSuffix("/**") { // Suffix is already well defined. return [FilenameMatcher(pattern: absolutPathPattern)] @@ -86,8 +150,6 @@ struct Glob { ] } - // MARK: Private - private static func expandGlobstar(pattern: URL) -> [URL] { guard pattern.path.contains("**") else { return [pattern] diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index c9e027bb94..6127a8b5c3 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -31,6 +31,7 @@ final class GlobTests: SwiftLintTestCase { XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) } + #if !os(Windows) func testMatchesOneCharacterInBracket() { let files = Glob.resolveGlob(mockPath.appending(path: "Level[01].swift")) XCTAssertEqual(files, [mockPath.appending(path: "Level0.swift")]) @@ -50,6 +51,7 @@ final class GlobTests: SwiftLintTestCase { let files = Glob.resolveGlob(mockPath.appending(path: "Level[a-z].swift")) XCTAssertTrue(files.isEmpty) } + #endif func testMatchesMultipleFiles() { let expectedFiles = [ @@ -88,9 +90,23 @@ final class GlobTests: SwiftLintTestCase { } func testCreateFilenameMatchers() { - func assertGlobMatch(root: String = "", pattern: String, filename: String) { + func assertGlobMatch(root: String = "", pattern: String, filename: String, + file: StaticString = #filePath, line: UInt = #line) { + #if os(Windows) + var root = root + var pattern = pattern + var filename = filename + if root.starts(with: "/") { + root = "C:" + root + } + if pattern.starts(with: "/") { + pattern = "C:" + pattern + } + filename = "C:" + filename + #endif + print(root, pattern, filename) let matchers = Glob.createFilenameMatchers(root: root, pattern: pattern) - XCTAssert(matchers.anyMatch(filename: filename)) + XCTAssert(matchers.anyMatch(filename: filename), file: file, line: line) } assertGlobMatch(root: "/a/b/", pattern: "c/*.swift", filename: "/a/b/c/d.swift") From 4fe7fd1784650ab76ce1a272dd1a01a1bac19716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 11 Jan 2026 00:28:21 +0100 Subject: [PATCH 04/15] Assume absolute path patterns --- .../Configuration+LintableFiles.swift | 2 +- Source/SwiftLintFramework/Helpers/Glob.swift | 22 +++-------- Tests/FileSystemAccessTests/GlobTests.swift | 38 +++++++------------ 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index d7ffebf6e3..87220d95cd 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -96,7 +96,7 @@ extension Configuration { } return .matching( matchers: excludedPaths.flatMap { - Glob.createFilenameMatchers(root: rootDirectory.path, pattern: $0.path) + Glob.createFilenameMatchers(pattern: $0.path) } ) } diff --git a/Source/SwiftLintFramework/Helpers/Glob.swift b/Source/SwiftLintFramework/Helpers/Glob.swift index 61b1282503..bf02a0dd3f 100644 --- a/Source/SwiftLintFramework/Helpers/Glob.swift +++ b/Source/SwiftLintFramework/Helpers/Glob.swift @@ -121,32 +121,20 @@ struct Glob { } #endif - static func createFilenameMatchers(root: String, pattern: String) -> [FilenameMatcher] { - var absolutPathPattern = pattern - #if os(Windows) - if !pattern.contains(":") { - // If the root is not already part of the pattern, prepend it. - absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern - } - #else - if !pattern.starts(with: "/") { - // If the root is not already part of the pattern, prepend it. - absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern - } - #endif + static func createFilenameMatchers(pattern: String) -> [FilenameMatcher] { if pattern.hasSuffix(".swift") || pattern.hasSuffix("/**") { // Suffix is already well defined. - return [FilenameMatcher(pattern: absolutPathPattern)] + return [FilenameMatcher(pattern: pattern)] } if pattern.hasSuffix("/") { // Matching all files in the folder. - return [FilenameMatcher(pattern: absolutPathPattern + "**")] + return [FilenameMatcher(pattern: pattern + "**")] } // The pattern could match files in the last folder in the path or all contained files if the last component // represents folders. return [ - FilenameMatcher(pattern: absolutPathPattern), - FilenameMatcher(pattern: absolutPathPattern + "/**"), + FilenameMatcher(pattern: pattern), + FilenameMatcher(pattern: pattern + "/**"), ] } diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index 6127a8b5c3..63a2e640c9 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -90,44 +90,34 @@ final class GlobTests: SwiftLintTestCase { } func testCreateFilenameMatchers() { - func assertGlobMatch(root: String = "", pattern: String, filename: String, - file: StaticString = #filePath, line: UInt = #line) { + func assertGlobMatch(pattern: String, filename: String, file: StaticString = #filePath, line: UInt = #line) { #if os(Windows) - var root = root var pattern = pattern var filename = filename - if root.starts(with: "/") { - root = "C:" + root - } - if pattern.starts(with: "/") { - pattern = "C:" + pattern - } + pattern = "C:" + pattern filename = "C:" + filename #endif - print(root, pattern, filename) - let matchers = Glob.createFilenameMatchers(root: root, pattern: pattern) + let matchers = Glob.createFilenameMatchers(pattern: pattern) XCTAssert(matchers.anyMatch(filename: filename), file: file, line: line) } - assertGlobMatch(root: "/a/b/", pattern: "c/*.swift", filename: "/a/b/c/d.swift") - assertGlobMatch(root: "/a", pattern: "**/*.swift", filename: "/a/b/c/d.swift") - assertGlobMatch(root: "/a", pattern: "**/*.swift", filename: "/a/b.swift") - assertGlobMatch(root: "/", pattern: "**/*.swift", filename: "/a/b.swift") - assertGlobMatch(root: "/", pattern: "a/**/b.swift", filename: "/a/b.swift") - assertGlobMatch(root: "/", pattern: "a/**/b.swift", filename: "/a/c/b.swift") - assertGlobMatch(root: "/", pattern: "**/*.swift", filename: "/a.swift") - assertGlobMatch(root: "/", pattern: "a/**/*.swift", filename: "/a/b/c.swift") - assertGlobMatch(root: "/", pattern: "a/**/*.swift", filename: "/a/b.swift") - assertGlobMatch(root: "/a/b", pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") - assertGlobMatch(root: "/a/", pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/a**/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/a**/*.swift", filename: "/a/b.swift") + assertGlobMatch(pattern: "/**/*.swift", filename: "/a/b.swift") + assertGlobMatch(pattern: "/a/**/b.swift", filename: "/a/b.swift") + assertGlobMatch(pattern: "/a/**/b.swift", filename: "/a/c/b.swift") + assertGlobMatch(pattern: "/**/*.swift", filename: "/a.swift") + assertGlobMatch(pattern: "/a/**/*.swift", filename: "/a/b/c.swift") + assertGlobMatch(pattern: "/a/**/*.swift", filename: "/a/b.swift") assertGlobMatch(pattern: "/a/b/c", filename: "/a/b/c/d.swift") assertGlobMatch(pattern: "/a/b/c/", filename: "/a/b/c/d.swift") assertGlobMatch(pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") assertGlobMatch(pattern: "/d.swift/*.swift", filename: "/d.swift/e.swift") assertGlobMatch(pattern: "/a/**", filename: "/a/b/c/d.swift") - assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/c/MyTest2.swift") - assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/MyTests/c.swift") + assertGlobMatch(pattern: "/**/*Test*", filename: "/a/b/c/MyTest2.swift") + assertGlobMatch(pattern: "/**/*Test*", filename: "/a/b/MyTests/c.swift") } // swiftlint:disable:next identifier_name From 54261e922fc593dd3cf0b3cbb6aaef151d828457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 18 Jan 2026 12:23:47 +0100 Subject: [PATCH 05/15] Use variable for system drive --- Tests/FileSystemAccessTests/GlobTests.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index 63a2e640c9..743f435e90 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -92,10 +92,12 @@ final class GlobTests: SwiftLintTestCase { func testCreateFilenameMatchers() { func assertGlobMatch(pattern: String, filename: String, file: StaticString = #filePath, line: UInt = #line) { #if os(Windows) - var pattern = pattern - var filename = filename - pattern = "C:" + pattern - filename = "C:" + filename + guard let driveLetter = ProcessInfo.processInfo.environment["SystemDrive"] else { + XCTFail("Cannot retrieve %SystemDrive% environment variable") + return + } + var pattern = driveLetter + pattern + var filename = driveLetter + filename #endif let matchers = Glob.createFilenameMatchers(pattern: pattern) XCTAssert(matchers.anyMatch(filename: filename), file: file, line: line) From 4a9fb3a3bc3b1bd82843bc74a324be8761d1ae99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Mon, 12 Jan 2026 19:03:42 +0100 Subject: [PATCH 06/15] Rename property --- .../SwiftLintCore/Extensions/URL+SwiftLint.swift | 2 +- Source/SwiftLintCore/Models/Baseline.swift | 4 ++-- Source/SwiftLintCore/Models/Correction.swift | 2 +- Source/SwiftLintCore/Models/Issue.swift | 14 ++++++++------ .../SwiftLintFramework/LintableFilesVisitor.swift | 2 +- .../Reporters/CodeClimateReporter.swift | 4 ++-- .../Reporters/GitHubActionsLoggingReporter.swift | 2 +- .../Reporters/GitLabJUnitReporter.swift | 2 +- .../Reporters/HTMLReporter.swift | 2 +- .../Reporters/RelativePathReporter.swift | 2 +- .../Reporters/SARIFReporter.swift | 4 ++-- .../Reporters/SonarQubeReporter.swift | 2 +- Tests/FileSystemAccessTests/ReporterTests.swift | 2 +- .../ConfigPathResolutionTests.swift | 2 +- 14 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift index e2343c6c98..1f1b595de7 100644 --- a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift @@ -45,7 +45,7 @@ public extension URL { /// /// > Warning: Use this representation only for displaying file paths to users. It is not /// suitable for file operations. - var relativeFilepath: String { + var relativeDisplayPath: String { let path = path.replacing(URL.cwd.path, with: "") if path.starts(with: "/") { return String(path.dropFirst()) diff --git a/Source/SwiftLintCore/Models/Baseline.swift b/Source/SwiftLintCore/Models/Baseline.swift index 2f417508a7..e0f2e03848 100644 --- a/Source/SwiftLintCore/Models/Baseline.swift +++ b/Source/SwiftLintCore/Models/Baseline.swift @@ -68,7 +68,7 @@ public struct Baseline: Equatable { /// - Returns: The new violations. public func filter(_ violations: [StyleViolation]) -> [StyleViolation] { guard let firstViolation = violations.first, - let baselineViolations = baseline[firstViolation.location.file?.relativeFilepath ?? ""], + let baselineViolations = baseline[firstViolation.location.file?.relativeDisplayPath ?? ""], baselineViolations.isNotEmpty else { return violations } @@ -168,7 +168,7 @@ private extension Sequence where Element == BaselineViolation { } func groupedByFile() -> ViolationsPerFile { - Dictionary(grouping: self) { $0.violation.location.file?.relativeFilepath ?? "" } + Dictionary(grouping: self) { $0.violation.location.file?.relativeDisplayPath ?? "" } } func groupedByRuleIdentifier(filteredBy existingViolations: [BaselineViolation] = []) -> ViolationsPerRule { diff --git a/Source/SwiftLintCore/Models/Correction.swift b/Source/SwiftLintCore/Models/Correction.swift index 91d815830e..48dfa40531 100644 --- a/Source/SwiftLintCore/Models/Correction.swift +++ b/Source/SwiftLintCore/Models/Correction.swift @@ -12,7 +12,7 @@ public struct Correction: Equatable, Sendable { /// The console-printable description for this correction. public var consoleDescription: String { let times = numberOfCorrections == 1 ? "time" : "times" - return "\(filePath?.relativeFilepath ?? ""): Corrected \(ruleName) \(numberOfCorrections) \(times)" + return "\(filePath?.relativeDisplayPath ?? ""): Corrected \(ruleName) \(numberOfCorrections) \(times)" } /// Memberwise initializer. diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index 849344c9ec..412040052b 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -183,22 +183,24 @@ public enum Issue: LocalizedError, Equatable { case let .fileNotFound(path): return "File at path '\(path)' not found." case let .fileNotReadable(path, id): - return "Cannot open or read file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." + return "Cannot open or read file at path '\(path?.relativeDisplayPath ?? "...")' within '\(id)' rule." case let .fileNotWritable(path): - return "Cannot write to file at path '\(path.relativeFilepath)'." + return "Cannot write to file at path '\(path.relativeDisplayPath)'." case let .indexingError(path, id): - return "Cannot index file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." + return "Cannot index file at path '\(path?.relativeDisplayPath ?? "...")' within '\(id)' rule." case let .missingCompilerArguments(path, id): return """ - Attempted to lint file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule \ + Attempted to lint file at path '\(path?.relativeDisplayPath ?? "...")' within '\(id)' rule \ without any compiler arguments. """ case let .missingCursorInfo(path, id): - return "Cannot get cursor info from file at path '\(path?.relativeFilepath ?? "...")' within '\(id)' rule." + return """ + Cannot get cursor info from file at path '\(path?.relativeDisplayPath ?? "...")' within '\(id)' rule. + """ case let .yamlParsing(message): return "Cannot parse YAML file: \(message)" case let .baselineNotReadable(path): - return "Cannot open or read the baseline file at path '\(path.relativeFilepath)'." + return "Cannot open or read the baseline file at path '\(path.relativeDisplayPath)'." } } } diff --git a/Source/SwiftLintFramework/LintableFilesVisitor.swift b/Source/SwiftLintFramework/LintableFilesVisitor.swift index 4700cc5d05..0065f1b82d 100644 --- a/Source/SwiftLintFramework/LintableFilesVisitor.swift +++ b/Source/SwiftLintFramework/LintableFilesVisitor.swift @@ -47,7 +47,7 @@ class CompilerInvocations { override func arguments(forFile path: URL?) -> Arguments { path.flatMap { path in compileCommands[path.filepath] ?? - compileCommands[path.relativeFilepath] + compileCommands[path.relativeDisplayPath] } ?? [] } } diff --git a/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift b/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift index 1f8bbb7d74..6efdeca08d 100644 --- a/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CodeClimateReporter.swift @@ -26,7 +26,7 @@ struct CodeClimateReporter: Reporter { "engine_name": "SwiftLint", "fingerprint": generateFingerprint(violation), "location": [ - "path": violation.location.file?.relativeFilepath ?? NSNull() as Any, + "path": violation.location.file?.relativeDisplayPath ?? NSNull() as Any, "lines": [ "begin": violation.location.line ?? NSNull() as Any, "end": violation.location.line ?? NSNull() as Any, @@ -39,7 +39,7 @@ struct CodeClimateReporter: Reporter { internal static func generateFingerprint(_ violation: StyleViolation) -> String { [ - "\(violation.location.file?.relativeFilepath ?? "")", + "\(violation.location.file?.relativeDisplayPath ?? "")", "\(violation.location.line ?? 0)", "\(violation.location.character ?? 0)", "\(violation.ruleIdentifier)", diff --git a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift index d2c8ed0789..cc7e66fc7c 100644 --- a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift +++ b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift @@ -19,7 +19,7 @@ struct GitHubActionsLoggingReporter: Reporter { // ::(warning|error) file={relative_path_to_file},line={:line},col={:character}::{content} [ "::\(violation.severity.rawValue) ", - "file=\(violation.location.file?.relativeFilepath ?? ""),", + "file=\(violation.location.file?.relativeDisplayPath ?? ""),", "line=\(violation.location.line ?? 1),", "col=\(violation.location.character ?? 1)::", violation.reason, diff --git a/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift b/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift index 6aec4ab98d..17ad7d841f 100644 --- a/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift +++ b/Source/SwiftLintFramework/Reporters/GitLabJUnitReporter.swift @@ -9,7 +9,7 @@ struct GitLabJUnitReporter: Reporter { static func generateReport(_ violations: [StyleViolation]) -> String { "\n" + violations.map({ violation -> String in - let fileName = (violation.location.file?.relativeFilepath ?? "").escapedForXML() + let fileName = (violation.location.file?.relativeDisplayPath ?? "").escapedForXML() let line = violation.location.line.map(String.init) let column = violation.location.character.map(String.init) diff --git a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift index 9aafce73de..9ae0797953 100644 --- a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift +++ b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift @@ -152,7 +152,7 @@ struct HTMLReporter: Reporter { private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String { let severity: String = violation.severity.rawValue.capitalized let location = violation.location - let file: String = (violation.location.file?.relativeFilepath ?? "").escapedForXML() + let file: String = (violation.location.file?.relativeDisplayPath ?? "").escapedForXML() let line: Int = location.line ?? 0 let character: Int = location.character ?? 0 return """ diff --git a/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift b/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift index ee69378aba..27512ebe73 100644 --- a/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift +++ b/Source/SwiftLintFramework/Reporters/RelativePathReporter.swift @@ -19,7 +19,7 @@ struct RelativePathReporter: Reporter { // {relative_path_to_file}{:line}{:character}: {error,warning}: {content} [ - "\(violation.location.file?.relativeFilepath ?? "")", + "\(violation.location.file?.relativeDisplayPath ?? "")", ":\(violation.location.line ?? 1)", ":\(violation.location.character ?? 1): ", "\(violation.severity.rawValue): ", diff --git a/Source/SwiftLintFramework/Reporters/SARIFReporter.swift b/Source/SwiftLintFramework/Reporters/SARIFReporter.swift index a94b35f51b..08e3d8a4dd 100644 --- a/Source/SwiftLintFramework/Reporters/SARIFReporter.swift +++ b/Source/SwiftLintFramework/Reporters/SARIFReporter.swift @@ -71,7 +71,7 @@ struct SARIFReporter: Reporter { return [ "physicalLocation": [ "artifactLocation": [ - "uri": location.file?.relativeFilepath ?? "" + "uri": location.file?.relativeDisplayPath ?? "" ], "region": [ "startLine": line, @@ -84,7 +84,7 @@ struct SARIFReporter: Reporter { return [ "physicalLocation": [ "artifactLocation": [ - "uri": location.file?.relativeFilepath ?? "" + "uri": location.file?.relativeDisplayPath ?? "" ], ], ] diff --git a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift index b15ec26dd2..2a701acb3f 100644 --- a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift +++ b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift @@ -21,7 +21,7 @@ struct SonarQubeReporter: Reporter { "ruleId": violation.ruleIdentifier, "primaryLocation": [ "message": violation.reason, - "filePath": violation.location.file?.relativeFilepath ?? "", + "filePath": violation.location.file?.relativeDisplayPath ?? "", "textRange": [ "startLine": violation.location.line ?? 1 ] as Any, diff --git a/Tests/FileSystemAccessTests/ReporterTests.swift b/Tests/FileSystemAccessTests/ReporterTests.swift index 71067c6531..79c6cbbfdc 100644 --- a/Tests/FileSystemAccessTests/ReporterTests.swift +++ b/Tests/FileSystemAccessTests/ReporterTests.swift @@ -204,7 +204,7 @@ final class ReporterTests: SwiftLintTestCase { reason: "Violation Reason") let result = RelativePathReporter.generateReport([violation]) XCTAssertFalse(result.contains(relativePath.filepath)) - XCTAssertTrue(result.contains(relativePath.relativeFilepath)) + XCTAssertTrue(result.contains(relativePath.relativeDisplayPath)) } func testSummaryReporter() { diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 8065c67260..062139a09b 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -28,7 +28,7 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { excludeByPrefix: false ) - return files.map { $0.path!.relativeFilepath }.sorted() + return files.map { $0.path!.relativeDisplayPath }.sorted() } func testParentChildSameDirectory() { From 05f1f65626b8bf4ba57d29e45bc21bbae732e8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Mon, 12 Jan 2026 19:04:08 +0100 Subject: [PATCH 07/15] Use relative display path in output for duplicated files --- Source/SwiftLintFramework/Configuration+CommandLine.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 85fae90371..599c913b16 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -137,13 +137,7 @@ extension Configuration { if !duplicateFileNames.contains(basename) { return basename } - - var pathComponents = path.pathComponents - for component in rootDirectory.pathComponents where pathComponents.first == component { - pathComponents.removeFirst() - } - - return pathComponents.reduce(URL(filePath: "/")) { $0.appending(path: $1) }.filepath + return path.relativeDisplayPath } private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]], From 5fdfdf2b553529ef8e6d65bf797fb5bdb3cf20a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Thu, 15 Jan 2026 23:07:28 +0100 Subject: [PATCH 08/15] Run test on all macOS versions in the same way --- .../ConfigPathResolutionTests.swift | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 062139a09b..733a088a91 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -171,7 +171,7 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { } #if !os(Windows) - func testUnicodePrivateUseAreaCharacterInPath() async throws { + func testUnicodePrivateUseAreaCharacterInPath() throws { let fixture = fixturePath("_8_unicode_private_use_area") let process = Process() @@ -181,24 +181,10 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { process.waitUntilExit() defer { try? FileManager.default.removeItem(at: fixture.appending(path: "App")) } - if #available(macOS 26, *) { - XCTAssertEqual( - lintableFilePaths(in: "_8_unicode_private_use_area/App"), - ["Resources/Settings.bundle/androidx.core:core-bundle.swift"] - ) - } else { - let console = await Issue.captureConsole { - XCTAssert(lintableFilePaths(in: "_8_unicode_private_use_area/App").isEmpty) - } - XCTAssert( - console.contains( - """ - error: File with URL 'androidx.core:core-bundle.swift' \ - cannot be represented as a file system path; skipping it - """ - ) - ) - } + XCTAssertEqual( + lintableFilePaths(in: "_8_unicode_private_use_area/App"), + ["Resources/Settings.bundle/androidx.core:core-bundle.swift"] + ) } #endif } From c2ce7bea77c6672990b3f711128cd84511f99851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 17 Jan 2026 12:20:46 +0100 Subject: [PATCH 09/15] Use URL enumerator --- .../Extensions/String+SwiftLint.swift | 10 ---- .../Extensions/URL+SwiftLint.swift | 12 ----- .../Configuration+LintableFiles.swift | 17 ++---- .../Extensions/FileManager+SwiftLint.swift | 53 ++++++++++++------- .../ConfigurationTests.swift | 2 +- .../ConfigPathResolutionTests.swift | 38 ++++++++++++- .../_9_symlinked_paths/.swiftlint.yml | 2 + .../Excluded/Excluded.swift | 0 .../_9_symlinked_paths/LinkToFile.swift | 1 + .../Resources/_9_symlinked_paths/LinkToFolder | 1 + .../Real/Folder/Nested.swift | 1 + .../_9_symlinked_paths/Real/Target.swift | 1 + 12 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 Tests/IntegrationTests/Resources/_9_symlinked_paths/.swiftlint.yml create mode 100644 Tests/IntegrationTests/Resources/_9_symlinked_paths/Excluded/Excluded.swift create mode 120000 Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFile.swift create mode 120000 Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFolder create mode 100644 Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Folder/Nested.swift create mode 100644 Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Target.swift diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index b6b7e6dfa5..c52d0c9764 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -66,16 +66,6 @@ public extension String { NSRange(location: 0, length: utf16.count) } - /// Returns a new string, converting the path to a canonical absolute path. - /// - /// > Important: This method might use an incorrect working directory internally. This can cause test failures - /// in Bazel builds but does not seem to cause trouble in production. - /// - /// - returns: A new `String`. - func absolutePathStandardized() -> String { - URL(filePath: bridge().standardizingPath.absolutePathRepresentation()).filepath - } - /// Count the number of occurrences of the given character in `self` /// - Parameter character: Character to count /// - Returns: Number of times `character` occurs in `self` diff --git a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift index 1f1b595de7..8f95638e6a 100644 --- a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift @@ -9,18 +9,6 @@ public extension URL { withUnsafeFileSystemRepresentation { String(cString: $0!) } } - var filepathGuarded: String? { - withUnsafeFileSystemRepresentation { ptr in - guard let ptr else { - Issue.genericError( - "File with URL '\(self)' cannot be represented as a file system path; skipping it" - ).print() - return nil - } - return String(cString: ptr) - } - } - var isSwiftFile: Bool { isFile && pathExtension == "swift" } diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index 87220d95cd..8bb9e2bf50 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -48,10 +48,7 @@ extension Configuration { // With no included paths, we lint everything in the given path. if includedPaths.isEmpty { - return fileManager.filesToLint( - inPath: path, - excluder: excluder - ) + return makeUnique(paths: fileManager.filesToLint(inPath: path, excluder: excluder)) } // With included paths, only lint them (after resolving globs). @@ -88,16 +85,8 @@ extension Configuration { return .noExclusion } if excludeByPrefix { - return .byPrefix( - prefixes: excludedPaths - .flatMap { Glob.resolveGlob($0) } - .map(\.path) - ) + return .byPrefix(prefixes: excludedPaths.flatMap(Glob.resolveGlob).map(\.path)) } - return .matching( - matchers: excludedPaths.flatMap { - Glob.createFilenameMatchers(pattern: $0.path) - } - ) + return .matching(matchers: excludedPaths.flatMap { Glob.createFilenameMatchers(pattern: $0.path) }) } } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 492014847f..0c07c89cc3 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -34,12 +34,11 @@ public enum Excluder { case noExclusion func excludes(path: URL) -> Bool { - let standardized = path.standardized.path - return switch self { + switch self { case let .matching(matchers): - matchers.contains(where: { $0.match(filename: standardized) }) + matchers.contains(where: { $0.match(filename: path.path) }) case let .byPrefix(prefixes): - prefixes.contains(where: { standardized.hasPrefix($0) }) + prefixes.contains(where: { path.path.hasPrefix($0) }) case .noExclusion: false } @@ -51,6 +50,17 @@ extension FileManager: @unchecked @retroactive Sendable {} #endif extension FileManager: LintableFileManager { + private static let enumeratorProperties: Set = [ + .isRegularFileKey, + .isSymbolicLinkKey, + ] + private static let enumeratorOptions: DirectoryEnumerationOptions = [ + .producesRelativePathURLs, + .skipsHiddenFiles, + .skipsPackageDescendants, + .skipsSubdirectoryDescendants, + ] + public func filesToLint(inPath path: URL, excluder: Excluder) -> [URL] { // If path is a file, filter and return it directly. if path.isSwiftFile { @@ -69,31 +79,38 @@ extension FileManager: LintableFileManager { } private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [URL] { - guard let enumerator = enumerator(atPath: absolutePath.filepath) else { + let absolutePath = absolutePath.standardized.resolvingSymlinksInPath() + let enumerator = enumerator( + at: absolutePath, + includingPropertiesForKeys: Array(Self.enumeratorProperties), + options: Self.enumeratorOptions + ) + guard let enumerator else { return [] } var files = [URL]() var directoriesToWalk = [URL]() - while let element = enumerator.nextObject() as? String { - let absoluteElementPath = element.url(relativeTo: absolutePath) - if absoluteElementPath.isFile { - if absoluteElementPath.pathExtension == "swift", - !excluder.excludes(path: absoluteElementPath) { - files.append(absoluteElementPath) + while var element = (enumerator.nextObject() as? URL)?.relative(to: absolutePath) { + var resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) + if resourceValues?.isSymbolicLink == true { + if excluder.excludes(path: element) { + continue } - } else { - enumerator.skipDescendants() - if !excluder.excludes(path: absoluteElementPath) { - directoriesToWalk.append(absoluteElementPath) + element.resolveSymlinksInPath() + resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) + } + if resourceValues?.isRegularFile == true { + if element.pathExtension == "swift", !excluder.excludes(path: element) { + files.append(element) } + } else if resourceValues != nil, !excluder.excludes(path: element) { + directoriesToWalk.append(element) } } - return files + directoriesToWalk.parallelFlatMap { - collectFiles(atPath: $0, excluder: excluder) - } + return files + directoriesToWalk.parallelFlatMap { collectFiles(atPath: $0, excluder: excluder) } } public func modificationDate(forFileAtPath path: URL) -> Date? { diff --git a/Tests/FileSystemAccessTests/ConfigurationTests.swift b/Tests/FileSystemAccessTests/ConfigurationTests.swift index f228ab2364..50a7aa5359 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests.swift @@ -644,7 +644,7 @@ final class ConfigurationTests: SwiftLintTestCase { // swiftlint:disable:this ty private extension Sequence where Element == String { func absolutePathsStandardized() -> [String] { // In Bazel builds, absolute paths might be prefixed with `/private`. - map { String($0.absolutePathStandardized().trimmingPrefix("/private")) } + map { URL(filePath: $0).standardizedFileURL.resolvingSymlinksInPath().relativeDisplayPath } } } diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 733a088a91..2dd96503ef 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -170,8 +170,43 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { ) } - #if !os(Windows) + func testSymlinkedFileAndFolderAreFollowed() throws { + #if os(Windows) + try XCTSkip("Symlinks in fixture folder are not supported on Windows") + #endif + + let expectedPaths = ["Real/Folder/Nested.swift", "Real/Target.swift"] + + // With symlinks + XCTAssertEqual(lintableFilePaths(in: "_9_symlinked_paths", configFile: ".swiftlint.yml"), expectedPaths) + + let fixture = fixturePath("_9_symlinked_paths") + let fileLink = fixture.appending(path: "LinkToFile.swift", directoryHint: .notDirectory) + var folderLink = fixture.appending(path: "LinkToFolder", directoryHint: .isDirectory) + let targetFile = fixture.appending(path: "Real/Target.swift", directoryHint: .notDirectory) + let targetFolder = fixture.appending(path: "Real/Folder", directoryHint: .isDirectory) + + let fileManager = FileManager.default + XCTAssert(fileManager.fileExists(atPath: fileLink.filepath)) + XCTAssert(fileManager.fileExists(atPath: folderLink.filepath)) + XCTAssert(try fileLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == true) + XCTAssert(try folderLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == true) + XCTAssertEqual(fileLink.resolvingSymlinksInPath(), targetFile) + + XCTAssertNotEqual(folderLink, targetFile) + folderLink.resolveSymlinksInPath() + XCTAssert(try folderLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == false) + XCTAssertEqual(folderLink, targetFolder) + + // Without symlinks + XCTAssertEqual(lintableFilePaths(in: "_9_symlinked_paths", configFile: ".swiftlint.yml"), expectedPaths) + } + func testUnicodePrivateUseAreaCharacterInPath() throws { + #if os(Windows) + try XCTSkip("Windows unzip does not support PUA characters in paths") + #endif + let fixture = fixturePath("_8_unicode_private_use_area") let process = Process() @@ -186,5 +221,4 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { ["Resources/Settings.bundle/androidx.core:core-bundle.swift"] ) } - #endif } diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/.swiftlint.yml b/Tests/IntegrationTests/Resources/_9_symlinked_paths/.swiftlint.yml new file mode 100644 index 0000000000..689adaeee6 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_9_symlinked_paths/.swiftlint.yml @@ -0,0 +1,2 @@ +excluded: + - Excluded diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/Excluded/Excluded.swift b/Tests/IntegrationTests/Resources/_9_symlinked_paths/Excluded/Excluded.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFile.swift b/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFile.swift new file mode 120000 index 0000000000..98ac0343d6 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFile.swift @@ -0,0 +1 @@ +Real/Target.swift \ No newline at end of file diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFolder b/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFolder new file mode 120000 index 0000000000..2817b15484 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_9_symlinked_paths/LinkToFolder @@ -0,0 +1 @@ +Real/Folder \ No newline at end of file diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Folder/Nested.swift b/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Folder/Nested.swift new file mode 100644 index 0000000000..f8137f0039 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Folder/Nested.swift @@ -0,0 +1 @@ +struct Nested {} diff --git a/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Target.swift b/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Target.swift new file mode 100644 index 0000000000..fb0a44f637 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_9_symlinked_paths/Real/Target.swift @@ -0,0 +1 @@ +struct Target {} From 6b76833ddc87984f4b6409e4020158d14bfcf2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 18 Jan 2026 13:59:31 +0100 Subject: [PATCH 10/15] Run ConfigPathResolutionTests with Bazel --- .../Extensions/FileManager+SwiftLint.swift | 1 - Tests/BUILD | 10 +++++----- Tests/IntegrationTests/ConfigPathResolutionTests.swift | 7 ++++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 0c07c89cc3..975716b377 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -79,7 +79,6 @@ extension FileManager: LintableFileManager { } private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [URL] { - let absolutePath = absolutePath.standardized.resolvingSymlinksInPath() let enumerator = enumerator( at: absolutePath, includingPropertiesForKeys: Array(Self.enumeratorProperties), diff --git a/Tests/BUILD b/Tests/BUILD index 4b2a087d27..ffdb0cac07 100644 --- a/Tests/BUILD +++ b/Tests/BUILD @@ -155,7 +155,7 @@ swift_library( name = "IntegrationTests.library", package_name = "SwiftLint", testonly = True, - srcs = ["IntegrationTests/IntegrationTests.swift"], + srcs = glob(["IntegrationTests/*.swift"]), copts = TARGETED_COPTS, # Set to strict once SwiftLintFramework is updated module_name = "IntegrationTests", deps = [ @@ -165,10 +165,10 @@ swift_library( swift_test( name = "IntegrationTests", - data = [ - "IntegrationTests/Resources/default_rule_configurations.yml", - "//:LintInputs", - ], + data = ["//:LintInputs"] + glob(["IntegrationTests/Resources/**"], allow_empty = True), visibility = ["//visibility:public"], deps = [":IntegrationTests.library"], + env = { + "SWIFTLINT_BAZEL_TEST": "true", + }, ) diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 2dd96503ef..0396a02e77 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -28,7 +28,8 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { excludeByPrefix: false ) - return files.map { $0.path!.relativeDisplayPath }.sorted() + // swiftlint:disable:next force_try + return files.map { $0.path!.path.replacing(try! Regex(".+/\(scenario)/"), with: "") }.sorted() } func testParentChildSameDirectory() { @@ -174,6 +175,10 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { #if os(Windows) try XCTSkip("Symlinks in fixture folder are not supported on Windows") #endif + try XCTSkipIf( + ProcessInfo.processInfo.environment["SWIFTLINT_BAZEL_TEST"] != nil, + "Bazel's sandboxed environment uses symlinks heavily breaking the fixture setup" + ) let expectedPaths = ["Real/Folder/Nested.swift", "Real/Target.swift"] From 0812d607b22361335a98d18c69bac8117cbb7c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Mon, 19 Jan 2026 23:58:42 +0100 Subject: [PATCH 11/15] Add changelog entry --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c53745bf8d..fbbec9cd24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,14 @@ ### Experimental -* None. +* SwiftLint can now be built and run on Windows. It is expected to work in the same way as + on other platforms. The only restrictions are missing support for `?[]` glob patterns in + include/exclude patterns and the requirement for `\n` as line ending in all linted files. + [compnerd](https://github.com/compnerd) + [roman-bcny](https://github.com/roman-bcny) + [SimplyDanny](https://github.com/SimplyDanny) + [#6351](https://github.com/realm/SwiftLint/issues/6351) + [#6352](https://github.com/realm/SwiftLint/issues/6352) ### Enhancements From 9ce4f6cba2ef1bdc1f0a8016d2b5c9a00d2a10db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 23 Jan 2026 21:00:01 +0100 Subject: [PATCH 12/15] Run all tests and lint codebase on Windows --- .github/workflows/test.yml | 9 ++++----- Tests/IntegrationTests/ConfigPathResolutionTests.swift | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1986451345..44b5168064 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,8 +89,7 @@ jobs: key: windows-${{ matrix.windows-version }}-spm-${{ env.SWIFT_VERSION }}-${{ env.SWIFT_BUILD }}-${{ hashFiles('Package.resolved', 'Package.swift') }} restore-keys: windows-${{ matrix.windows-version }}-spm-${{ env.SWIFT_VERSION }}-${{ env.SWIFT_BUILD }}- path: .build - - name: Build all targets - run: swift build --build-tests - - name: Run selected tests - run: swift test --skip IntegrationTests --skip FileSystemAccessTests --skip FrameworkTests --skip BuiltInRulesTests - # To be extended with test execution and linting ... + - name: Run tests + run: swift test --parallel + - name: Lint codebase + run: swift run swiftlint lint --strict diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 0396a02e77..5be5248dcc 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -173,7 +173,7 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { func testSymlinkedFileAndFolderAreFollowed() throws { #if os(Windows) - try XCTSkip("Symlinks in fixture folder are not supported on Windows") + throw XCTSkip("Symlinks in fixture folder are not supported on Windows") #endif try XCTSkipIf( ProcessInfo.processInfo.environment["SWIFTLINT_BAZEL_TEST"] != nil, @@ -209,7 +209,7 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { func testUnicodePrivateUseAreaCharacterInPath() throws { #if os(Windows) - try XCTSkip("Windows unzip does not support PUA characters in paths") + throw XCTSkip("Windows unzip does not support PUA characters in paths") #endif let fixture = fixturePath("_8_unicode_private_use_area") From 18958ae69baf71b2c82c9f18669156ca7d22630e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 24 Jan 2026 09:26:28 +0100 Subject: [PATCH 13/15] Do not just skip hidden files --- Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 975716b377..ae189fbbb5 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -56,7 +56,6 @@ extension FileManager: LintableFileManager { ] private static let enumeratorOptions: DirectoryEnumerationOptions = [ .producesRelativePathURLs, - .skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants, ] From 04c128cf3f82c875be92e4454d5bf96d1670418a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 24 May 2026 15:04:44 +0200 Subject: [PATCH 14/15] Avoid relative path computation for all files --- .../Extensions/FileManager+SwiftLint.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index ae189fbbb5..9f0aaa318a 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -90,13 +90,17 @@ extension FileManager: LintableFileManager { var files = [URL]() var directoriesToWalk = [URL]() - while var element = (enumerator.nextObject() as? URL)?.relative(to: absolutePath) { + for case var element as URL in enumerator { var resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) if resourceValues?.isSymbolicLink == true { if excluder.excludes(path: element) { continue } element.resolveSymlinksInPath() + element = URL( + fileURLWithPath: element.lastPathComponent, + relativeTo: element.deletingLastPathComponent() + ) resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) } if resourceValues?.isRegularFile == true { From add49e7cfb57ad948673d8027bf71951de233b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 24 May 2026 19:15:41 +0200 Subject: [PATCH 15/15] Go with standardization only --- Source/SwiftLintCore/Extensions/URL+SwiftLint.swift | 2 +- .../Extensions/FileManager+SwiftLint.swift | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift index 8f95638e6a..3b96c9fcfb 100644 --- a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift @@ -34,7 +34,7 @@ public extension URL { /// > Warning: Use this representation only for displaying file paths to users. It is not /// suitable for file operations. var relativeDisplayPath: String { - let path = path.replacing(URL.cwd.path, with: "") + let path = path.replacing(Self.cwd.path, with: "") if path.starts(with: "/") { return String(path.dropFirst()) } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 9f0aaa318a..de2cf48166 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -55,7 +55,6 @@ extension FileManager: LintableFileManager { .isSymbolicLinkKey, ] private static let enumeratorOptions: DirectoryEnumerationOptions = [ - .producesRelativePathURLs, .skipsPackageDescendants, .skipsSubdirectoryDescendants, ] @@ -93,14 +92,11 @@ extension FileManager: LintableFileManager { for case var element as URL in enumerator { var resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) if resourceValues?.isSymbolicLink == true { + element = element.standardizedFileURL if excluder.excludes(path: element) { continue } element.resolveSymlinksInPath() - element = URL( - fileURLWithPath: element.lastPathComponent, - relativeTo: element.deletingLastPathComponent() - ) resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties) } if resourceValues?.isRegularFile == true {