Skip to content

Commit 972df5d

Browse files
committed
Add excludedDependencies parameter to unusedDependencies rule
1 parent 47ad8ac commit 972df5d

File tree

3 files changed

+85
-60
lines changed

3 files changed

+85
-60
lines changed

Sources/SPMGraphConfigSetup/Resources/SPMGraphConfig.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ public extension Array where Element == SPMGraphConfig.Lint.Rule {
6767
.abcFeatureModuleShouldNotDependOnThirdParties,
6868
.ruleThatChecksTheSourceFilesContent,
6969
// Example of how to use built in rules with your own conditions.
70-
// Note that `.liveModuleLiveDependency()` and `.baseOrInterfaceModuleLiveDependency()` are
70+
// Note that `.unusedDependencies()`, `.liveModuleLiveDependency()` and `.baseOrInterfaceModuleLiveDependency()` are
7171
// enabled by default as part of the `.default` ones, so consider removing `.default` and picking the
7272
// ones you wish to use!
73+
.unusedDependencies(
74+
excludedDependencies: ["DependencyToExclude"] // Exclude specific dependencies
75+
),
7376
.liveModuleLiveDependency(
7477
isLiveModule: {
7578
$0.name.hasSuffix("Implementation")

Sources/SPMGraphDescriptionInterface/SPMGraphConfigInterface.swift

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,10 @@ typealias Validate = (Package, _ excludedSuffixes: [String]) -> [LocalizedError]
102102
public extension Array where Element == SPMGraphConfig.Lint.Rule {
103103
/// The default lint rules for users of the spmgraph lint functionality.
104104
///
105-
/// - note: **Most default rules allow for customization**, i.e. `liveModuleLiveDependency` can be used
106-
/// directly when setting up your `SPMGraphConfig.swift` and with a custom implementation of both the `isLiveModule` and
107-
/// the `excludedDependencies` parameters.
105+
/// - note: **Most default rules allow for customization**, i.e. `liveModuleLiveDependency` and `unusedDependencies` can be used
106+
/// directly when setting up your `SPMGraphConfig.swift` with custom parameters such as `excludedDependencies`.
108107
static let `default`: [SPMGraphConfig.Lint.Rule] = [
109-
.unusedDependencies,
108+
.unusedDependencies(),
110109
.liveModuleLiveDependency(),
111110
.baseOrInterfaceModuleLiveDependency(),
112111
]
@@ -212,61 +211,69 @@ public extension SPMGraphConfig.Lint.Rule {
212211
/// - note: For `@_exported` usages, there will be an error in case only the exported module is used.
213212
/// For example, module Networking exports module NetworkingHelpers, if only NetworkingHelpers is used by a target there will be
214213
/// a lint error, while if both Networking and NetworkingHelpers are used there will be no error.
215-
static let unusedDependencies = Self(
216-
id: "unusedDependencies",
217-
name: "Unused linked dependencies",
218-
abstract: """
219-
To keep the project clean and avoid long compile times, a Module should not have any unused dependencies.
220-
221-
- Note: It does blindly expects the target to match the product name, and doesn't yet consider
222-
the multiple targets that compose a product (open improvement).
223-
224-
- Note: For `@_exported` usages, there will be an error in case only the exported module is used.
225-
For example, module Networking exports module NetworkingHelpers, if only NetworkingHelpers is used by a target
226-
there will be a lint error, while if both Networking and NetworkingHelpers are used there will be no error.
227-
""",
228-
validate: { package, excludedSuffixes in
229-
let errors: [SPMGraphConfig.Lint.Error] = package.modules
230-
.filter { !$0.containsOneOf(suffixes: excludedSuffixes) && !$0.isFeature }
231-
.sorted()
232-
.compactMap { module in
233-
let dependencies = module
234-
.dependenciesFilteringOutLiveInUITestSupport
235-
.filter { dependency in
236-
let isExcluded = dependency.containsOneOf(suffixes: excludedSuffixes)
237-
return !isExcluded && dependency.shouldBeImported
238-
}
239-
let swiftFiles = try? findSwiftFiles(in: module.path.pathString)
240-
241-
return dependencies.compactMap { dependency in
242-
let filePaths = swiftFiles ?? []
243-
var isDependencyUsed = false
244-
for filePath in filePaths {
245-
let fileContent = try? String(contentsOfFile: filePath, encoding: .utf8)
246-
let regexPattern =
247-
"import (enum |struct |class )?(\\b\(NSRegularExpression.escapedPattern(for: dependency.name))\\b)"
248-
if let regex = try? NSRegularExpression(pattern: regexPattern, options: []) {
249-
let range = NSRange(location: 0, length: fileContent?.utf16.count ?? 0)
250-
let match = regex.firstMatch(in: fileContent ?? "", options: [], range: range)
251-
if match != nil {
252-
isDependencyUsed = true
253-
break
214+
///
215+
/// - Parameters:
216+
/// - excludedDependencies: A list of dependency names that should be excluded from unused dependency checks (e.g., umbrella dependencies).
217+
static func unusedDependencies(
218+
excludedDependencies: [String] = []
219+
) -> Self {
220+
Self(
221+
id: "unusedDependencies",
222+
name: "Unused linked dependencies",
223+
abstract: """
224+
To keep the project clean and avoid long compile times, a Module should not have any unused dependencies.
225+
226+
- Note: It does blindly expects the target to match the product name, and doesn't yet consider
227+
the multiple targets that compose a product (open improvement).
228+
229+
- Note: For `@_exported` usages, there will be an error in case only the exported module is used.
230+
For example, module Networking exports module NetworkingHelpers, if only NetworkingHelpers is used by a target
231+
there will be a lint error, while if both Networking and NetworkingHelpers are used there will be no error.
232+
""",
233+
validate: { package, excludedSuffixes in
234+
let errors: [SPMGraphConfig.Lint.Error] = package.modules
235+
.filter { !$0.containsOneOf(suffixes: excludedSuffixes) && !$0.isFeature }
236+
.sorted()
237+
.compactMap { module in
238+
let dependencies = module
239+
.dependenciesFilteringOutLiveInUITestSupport
240+
.filter { dependency in
241+
let isExcluded = dependency.containsOneOf(suffixes: excludedSuffixes)
242+
let isExcludedDependency = excludedDependencies.contains(dependency.name)
243+
return !isExcluded && !isExcludedDependency && dependency.shouldBeImported
244+
}
245+
let swiftFiles = try? findSwiftFiles(in: module.path.pathString)
246+
247+
return dependencies.compactMap { dependency in
248+
let filePaths = swiftFiles ?? []
249+
var isDependencyUsed = false
250+
for filePath in filePaths {
251+
let fileContent = try? String(contentsOfFile: filePath, encoding: .utf8)
252+
let regexPattern =
253+
"import (enum |struct |class )?(\\b\(NSRegularExpression.escapedPattern(for: dependency.name))\\b)"
254+
if let regex = try? NSRegularExpression(pattern: regexPattern, options: []) {
255+
let range = NSRange(location: 0, length: fileContent?.utf16.count ?? 0)
256+
let match = regex.firstMatch(in: fileContent ?? "", options: [], range: range)
257+
if match != nil {
258+
isDependencyUsed = true
259+
break
260+
}
254261
}
255262
}
256-
}
257263

258-
return isDependencyUsed
259-
? nil
260-
: SPMGraphConfig.Lint.Error.unusedDependencies(
261-
moduleName: module.name,
262-
dependencyName: dependency.name
263-
)
264+
return isDependencyUsed
265+
? nil
266+
: SPMGraphConfig.Lint.Error.unusedDependencies(
267+
moduleName: module.name,
268+
dependencyName: dependency.name
269+
)
270+
}
264271
}
265-
}
266-
.flatMap { $0 }
267-
return errors
268-
}
269-
)
272+
.flatMap { $0 }
273+
return errors
274+
}
275+
)
276+
}
270277
}
271278

272279
private extension SPMGraphConfig.Lint.Rule {

Tests/SPMGraphDescriptionInterfaceTests/SPMGraphConfigTests.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ struct SPMGraphConfigTests {
188188
@Test("It has the correct properties")
189189
func testRuleProperties() {
190190
// GIVEN
191-
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies
191+
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies()
192192

193193
// THEN
194194
#expect(rule.id == "unusedDependencies")
@@ -198,7 +198,7 @@ struct SPMGraphConfigTests {
198198
To keep the project clean and avoid long compile times, a Module should not have any unused dependencies.
199199
200200
- Note: It does blindly expects the target to match the product name, and doesn't yet consider
201-
the multiple targets that compose a product (open improvement).
201+
the multiple targets that compose a product (open improvement).
202202
203203
- Note: For `@_exported` usages, there will be an error in case only the exported module is used.
204204
For example, module Networking exports module NetworkingHelpers, if only NetworkingHelpers is used by a target
@@ -210,7 +210,7 @@ struct SPMGraphConfigTests {
210210
@Test("Validate detects unused dependencies")
211211
func testValidateDetectsUnusedDependencies() async throws {
212212
// GIVEN
213-
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies
213+
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies()
214214

215215
// WHEN
216216
let package = try await loadFixturePackage()
@@ -233,7 +233,7 @@ struct SPMGraphConfigTests {
233233
@Test("Validate with excluded suffixes ignores matching modules")
234234
func testValidateWithExcludedSuffixes() async throws {
235235
// GIVEN
236-
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies
236+
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies()
237237

238238
// WHEN - Exclude modules with "WithUnusedDep" suffix
239239
let package = try await loadFixturePackage()
@@ -242,6 +242,21 @@ struct SPMGraphConfigTests {
242242
// THEN
243243
#expect(errors.isEmpty, "BaseModule should be ignored")
244244
}
245+
246+
@Test("Validate with excluded dependencies ignores specific dependencies")
247+
func testValidateWithExcludedDependencies() async throws {
248+
// GIVEN
249+
let rule = SPMGraphConfig.Lint.Rule.unusedDependencies(
250+
excludedDependencies: ["BaseModule"]
251+
)
252+
253+
// WHEN
254+
let package = try await loadFixturePackage()
255+
let errors = rule.validate(package, [])
256+
257+
// THEN
258+
#expect(errors.isEmpty, "BaseModule should be excluded from unused dependency checks")
259+
}
245260
}
246261

247262
@Suite("Default rules configuration")

0 commit comments

Comments
 (0)