Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Linting rules are defined in `Source/SwiftLintBuiltInRules/Rules`. If someone me

User-facing changes must be documented in the `CHANGELOG.md` file, which is organized by version. New entries always go into the "Main" section. They give credit to the person who has made the change and they reference the issue which has been fixed by the change.

All changes on configuration options must be reflected in `Tests/IntegrationTests/default_rule_configurations.yml`. This can be achieved by running `swift run swiftlint-dev rules register`. Running this command is also necessary when new rules got added or removed to (un-)register them from/in the list of built-in rules and tests verifying all examples in rule descriptions.
All changes on configuration options must be reflected in `Tests/IntegrationTests/Resources/default_rule_configurations.yml`. This can be achieved by running `swift run swiftlint-dev rules register`. Running this command is also necessary when new rules got added or removed to (un-)register them from/in the list of built-in rules and tests verifying all examples in rule descriptions.

For some rules, there are dedicated tests in `Tests/BuiltInRulesTests`. However, they are typically not required as all the examples in the rule descriptions are automatically tested. The examples in the rule descriptions are also used to generate documentation for the rules. If an example presents a very pathological case, that's helpful for testing but not for user documentation, you can add the `excludeFromDocumentation: true` parameter to the example initializer. Important is that all examples in the rule description are verified by running `<RuleName>RuleGeneratedTests` for rule modified rules.

Expand Down
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ excluded:
- assets
- Tests/BuiltInRulesTests/Resources
- Tests/FileSystemAccessTests/Resources
- Tests/IntegrationTests/Resources

# Enabled/disabled rules
analyzer_rules:
Expand Down
1 change: 1 addition & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ swift_library(
visibility = ["//visibility:public"],
deps = [
":Yams.wrapper",
"@FilenameMatcher",
"@SourceKittenFramework",
"@SwiftSyntax//:SwiftIDEUtils_opt",
"@SwiftSyntax//:SwiftOperators_opt",
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
formatting for function and method call arguments.
[GandaLF2006](https://github.com/GandaLF2006)

* Improve performance when exclude patterns resolve to a large set of files.
[SimplyDanny](https://github.com/SimplyDanny)
[#5018](https://github.com/realm/SwiftLint/issues/5018)
[#5207](https://github.com/realm/SwiftLint/issues/5207)
[#5953](https://github.com/realm/SwiftLint/issues/5953)
[#6084](https://github.com/realm/SwiftLint/issues/6084)
[#6259](https://github.com/realm/SwiftLint/issues/6259)

### Bug Fixes

* Fix `line_length` rule incorrectly ignoring function bodies when
Expand Down
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bazel_dep(name = "rules_swift", version = "3.4.0", max_compatibility_level = 3,

bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "SourceKittenFramework")
bazel_dep(name = "swift_argument_parser", version = "1.6.2", repo_name = "SwiftArgumentParser")
bazel_dep(name = "swift-filename-matcher", version = "2.0.1", repo_name = "FilenameMatcher")
bazel_dep(name = "yams", version = "6.2.0", repo_name = "Yams")

swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod")
Expand Down
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ let package = Package(
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")),
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", .upToNextMajor(from: "0.2.0")),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.9.0")),
.package(url: "https://github.com/ileitch/swift-filename-matcher", .upToNextMinor(from: "2.0.1")),
],
targets: [
.executableTarget(
Expand All @@ -65,6 +66,7 @@ let package = Package(
.target(
name: "SwiftLintFramework",
dependencies: [
.product(name: "FilenameMatcher", package: "swift-filename-matcher"),
"SwiftLintBuiltInRules",
"SwiftLintCore",
"SwiftLintExtraRules",
Expand Down Expand Up @@ -96,6 +98,7 @@ let package = Package(
dependencies: [
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux, .windows])),
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
.product(name: "FilenameMatcher", package: "swift-filename-matcher"),
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
Expand Down Expand Up @@ -167,6 +170,7 @@ let package = Package(
.testTarget(
name: "FileSystemAccessTests",
dependencies: [
.product(name: "FilenameMatcher", package: "swift-filename-matcher"),
"SwiftLintFramework",
"TestHelpers",
"SwiftLintCoreMacros",
Expand Down Expand Up @@ -203,7 +207,7 @@ let package = Package(
"TestHelpers",
],
exclude: [
"default_rule_configurations.yml"
"Resources",
],
swiftSettings: swiftFeatures + targetedConcurrency // Set to strict once SwiftLintFramework is updated
),
Expand Down
3 changes: 3 additions & 0 deletions Source/SwiftLintCore/Extensions/String+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public extension String {

/// 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(fileURLWithPath: bridge().standardizingPath.absolutePathRepresentation()).filepath
Expand Down
23 changes: 9 additions & 14 deletions Source/SwiftLintFramework/Configuration+CommandLine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,12 @@ extension Configuration {
guard options.forceExclude else {
return files
}

let scriptInputPaths = files.compactMap(\.path)

if options.useExcludingByPrefix {
return filterExcludedPathsByPrefix(in: scriptInputPaths)
.map(SwiftLintFile.init(pathDeferringReading:))
}
return filterExcludedPaths(excludedPaths(), in: scriptInputPaths)
.map(SwiftLintFile.init(pathDeferringReading:))
return (
visitor.options.useExcludingByPrefix
? filterExcludedPathsByPrefix(in: scriptInputPaths)
: filterExcludedPaths(in: scriptInputPaths)
).map(SwiftLintFile.init(pathDeferringReading:))
}
if !options.quiet {
let filesInfo: String
Expand All @@ -272,14 +269,12 @@ extension Configuration {

queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)")
}
let excludeLintableFilesBy = options.useExcludingByPrefix
? Configuration.ExcludeBy.prefix
: .paths(excludedPaths: excludedPaths())
return options.paths.flatMap {
return visitor.options.paths.flatMap {
self.lintableFiles(
inPath: $0,
forceExclude: options.forceExclude,
excludeBy: excludeLintableFilesBy)
forceExclude: visitor.options.forceExclude,
excludeByPrefix: visitor.options.useExcludingByPrefix
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import FilenameMatcher
import Foundation

extension Configuration {
public enum ExcludeBy {
case prefix
case paths(excludedPaths: [String])
}

// MARK: Lintable Paths

/// Returns the files that can be linted by SwiftLint in the specified parent path.
///
/// - parameter path: The parent path in which to search for lintable files. Can be a directory or a
/// file.
/// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if
/// `path` is an exact match.
/// - parameter excludeByPrefix: Whether or not uses excluding by prefix algorithm.
/// - parameter excludeByPrefix: Whether or not it uses the exclude-by-prefix algorithm.
///
/// - returns: Files to lint.
public func lintableFiles(inPath path: String,
forceExclude: Bool,
excludeBy: ExcludeBy) -> [SwiftLintFile] {
lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy)
excludeByPrefix: Bool) -> [SwiftLintFile] {
lintablePaths(inPath: path, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix)
.parallelCompactMap {
SwiftLintFile(pathDeferringReading: $0)
}
Expand All @@ -31,63 +28,55 @@ extension Configuration {
/// file.
/// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if
/// `path` is an exact match.
/// - parameter excludeByPrefix: Whether or not uses excluding by prefix algorithm.
/// - parameter excludeByPrefix: Whether or not it uses the exclude-by-prefix algorithm.
/// - parameter fileManager: The lintable file manager to use to search for lintable files.
///
/// - returns: Paths for files to lint.
internal func lintablePaths(
inPath path: String,
forceExclude: Bool,
excludeBy: ExcludeBy,
excludeByPrefix: Bool,
fileManager: some LintableFileManager = FileManager.default
) -> [String] {
if fileManager.isFile(atPath: path) {
let file = fileManager.filesToLint(inPath: path, rootDirectory: nil)
if forceExclude {
switch excludeBy {
case .prefix:
return filterExcludedPathsByPrefix(in: [path.absolutePathStandardized()])
case .paths(let excludedPaths):
return filterExcludedPaths(excludedPaths, in: [path.absolutePathStandardized()])
}
return excludeByPrefix
? filterExcludedPathsByPrefix(in: file)
: filterExcludedPaths(in: file)
}
// If path is a file and we're not forcing excludes, skip filtering with excluded/included paths
return [path]
return file
}

let pathsForPath = includedPaths.isEmpty ? fileManager.filesToLint(inPath: path, rootDirectory: nil) : []
let includedPaths = includedPaths
.flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }

switch excludeBy {
case .prefix:
return filterExcludedPathsByPrefix(in: pathsForPath, includedPaths)
case .paths(let excludedPaths):
return filterExcludedPaths(excludedPaths, in: pathsForPath, includedPaths)
}
return excludeByPrefix
? filterExcludedPathsByPrefix(in: pathsForPath + includedPaths)
: filterExcludedPaths(in: pathsForPath + includedPaths)
}

/// Returns an array of file paths after removing the excluded paths as defined by this configuration.
///
/// - parameter fileManager: The lintable file manager to use to expand the excluded paths into all matching paths.
/// - parameter paths: The input paths to filter.
/// - parameter paths: The input paths to filter.
///
/// - returns: The input paths after removing the excluded paths.
public func filterExcludedPaths(
_ excludedPaths: [String],
in paths: [String]...
) -> [String] {
let allPaths = paths.flatMap(\.self)
public func filterExcludedPaths(in paths: [String]) -> [String] {
#if os(Linux)
let result = NSMutableOrderedSet(capacity: allPaths.count)
result.addObjects(from: allPaths)
let result = NSMutableOrderedSet(capacity: paths.count)
result.addObjects(from: paths)
#else
let result = NSMutableOrderedSet(array: allPaths)
let result = NSOrderedSet(array: paths)
#endif

result.minusSet(Set(excludedPaths))
// swiftlint:disable:next force_cast
return result.map { $0 as! String }
let exclusionPatterns = excludedPaths.flatMap {
Glob.createFilenameMatchers(root: rootDirectory, pattern: $0)
}
return result.array
.parallelCompactMap { exclusionPatterns.anyMatch(filename: $0 as! String) ? nil : $0 as? String }
// swiftlint:disable:previous force_cast
}

/// Returns the file paths that are excluded by this configuration using filtering by absolute path prefix.
Expand All @@ -96,25 +85,12 @@ extension Configuration {
/// algorithm `filterExcludedPaths`.
///
/// - returns: The input paths after removing the excluded paths.
public func filterExcludedPathsByPrefix(in paths: [String]...) -> [String] {
let allPaths = paths.flatMap(\.self)
public func filterExcludedPathsByPrefix(in paths: [String]) -> [String] {
let excludedPaths = excludedPaths
.parallelFlatMap { @Sendable in Glob.resolveGlob($0) }
.parallelFlatMap { Glob.resolveGlob($0) }
.map { $0.absolutePathStandardized() }
return allPaths.filter { path in
return paths.filter { path in
!excludedPaths.contains { path.hasPrefix($0) }
}
}

/// Returns the file paths that are excluded by this configuration after expanding them using the specified file
/// manager.
///
/// - parameter fileManager: The file manager to get child paths in a given parent location.
///
/// - returns: The expanded excluded file paths.
public func excludedPaths(fileManager: some LintableFileManager = FileManager.default) -> [String] {
excludedPaths
.flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }
}
}
24 changes: 24 additions & 0 deletions Source/SwiftLintFramework/Helpers/Glob.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import FilenameMatcher
import Foundation
import SourceKittenFramework

Expand Down Expand Up @@ -64,6 +65,29 @@ struct Glob {
.map { $0.absolutePathStandardized() }
}

static func createFilenameMatchers(root: String, pattern: String) -> [FilenameMatcher] {
var absolutPathPattern = pattern
if !pattern.starts(with: root) {
// If the root is not already part of the pattern, prepend it.
absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern
}
absolutPathPattern = absolutPathPattern.absolutePathStandardized()
if pattern.hasSuffix(".swift") || pattern.hasSuffix("/**") {
// Suffix is already well defined.
return [FilenameMatcher(pattern: absolutPathPattern)]
}
if pattern.hasSuffix("/") {
// Matching all files in the folder.
return [FilenameMatcher(pattern: absolutPathPattern + "**")]
}
// 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 + "/**"),
]
}

// MARK: Private

private static func expandGlobstar(pattern: String) -> [String] {
Expand Down
1 change: 1 addition & 0 deletions Source/swiftlint-dev/Rules+Register.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ private extension SwiftLintDev.Rules.Register {
.write(
to: testsParentDirectory
.appendingPathComponent("IntegrationTests", isDirectory: true)
.appendingPathComponent("Resources", isDirectory: true)
.appendingPathComponent("default_rule_configurations.yml", isDirectory: false),
atomically: true,
encoding: .utf8
Expand Down
2 changes: 1 addition & 1 deletion Tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ swift_library(
swift_test(
name = "IntegrationTests",
data = [
"IntegrationTests/default_rule_configurations.yml",
"IntegrationTests/Resources/default_rule_configurations.yml",
"//:LintInputs",
],
visibility = ["//visibility:public"],
Expand Down
4 changes: 4 additions & 0 deletions Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ internal extension ConfigurationTests {
static var remoteConfigLocalRef: String { level0.stringByAppendingPathComponent("RemoteConfig/LocalRef") }
static var remoteConfigCycle: String { level0.stringByAppendingPathComponent("RemoteConfig/Cycle") }
static var emptyFolder: String { level0.stringByAppendingPathComponent("EmptyFolder") }

static var exclusionTests: String { testResourcesPath.stringByAppendingPathComponent("ExclusionTests") }
static var directory: String { exclusionTests.stringByAppendingPathComponent("directory") }
static var directoryExcluded: String { directory.stringByAppendingPathComponent("excluded") }
}

// MARK: YAML File Paths
Expand Down
Loading