Skip to content

Commit a0b973e

Browse files
committed
Add line number tracking for unused dependencies in Package.swift
Enhanced PackageParser to capture exact line numbers where dependencies are declared and updated output formatters to include line numbers for unused dependency warnings.
1 parent 192a429 commit a0b973e

File tree

6 files changed

+120
-16
lines changed

6 files changed

+120
-16
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fixed incomplete line number capture in Xcode and GitHub Actions output formats
2121
- Now reports all occurrences of missing dependencies across all source files with correct line numbers
2222
- Removed premature loop termination that was causing missing line number information
23+
- Added line number tracking for unused dependencies in Package.swift files
24+
- Enhanced dependency parsing to capture exact line numbers where dependencies are declared
2325

2426
### Technical Details
2527
- Enhanced `ImportInfo` model with line number tracking
28+
- Added `DependencyInfo` model to track dependency line numbers in Package.swift
2629
- Added `XcodeOutput` module for Xcode-compatible format (`file:line: error: message`)
2730
- Added `GitHubActionsOutput` module for GitHub Actions format (`::error file=path,line=N::message`)
2831
- Updated `ImportScanner` to capture line numbers during regex matching
32+
- Enhanced `PackageParser` with `parseDependencyListWithLineNumbers()` and `findDependencyLineNumber()` methods
2933
- Extended `DependencyAnalyzer` with `generateXcodeReport()` and `generateGitHubActionsReport()` methods
3034
- Fixed loop logic in `generateXcodeReport()` and `generateGitHubActionsReport()` to report all import occurrences
35+
- Updated `Target` model to include `dependencyInfo` array with line number tracking
3136
- All existing functionality preserved with backward compatibility
3237

3338
## [1.0.0] - 2025-07-16

Sources/SwiftDependencyAuditLib/DependencyAnalyzer.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,11 @@ public actor DependencyAnalyzer {
231231
// Generate warnings for unused dependencies
232232
for unusedDep in result.unusedDependencies.sorted() {
233233
let packageFile = URL(fileURLWithPath: packagePath).appendingPathComponent("Package.swift").path
234+
let lineNumber = result.target.dependencyInfo.first { $0.name == unusedDep }?.lineNumber
234235
let message = XcodeOutput.unusedDependencyWarning(
235236
dependency: unusedDep,
236-
packageFile: packageFile
237+
packageFile: packageFile,
238+
line: lineNumber
237239
)
238240
output.append(message)
239241
}
@@ -266,9 +268,11 @@ public actor DependencyAnalyzer {
266268
// Generate warnings for unused dependencies
267269
for unusedDep in result.unusedDependencies.sorted() {
268270
let packageFile = URL(fileURLWithPath: packagePath).appendingPathComponent("Package.swift").path
271+
let lineNumber = result.target.dependencyInfo.first { $0.name == unusedDep }?.lineNumber
269272
let message = GitHubActionsOutput.unusedDependencyWarning(
270273
dependency: unusedDep,
271-
packageFile: packageFile
274+
packageFile: packageFile,
275+
line: lineNumber
272276
)
273277
output.append(message)
274278
}

Sources/SwiftDependencyAuditLib/GitHubActionsOutput.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ public struct GitHubActionsOutput {
3434
)
3535
}
3636

37-
public static func unusedDependencyWarning(dependency: String, packageFile: String) -> String {
37+
public static func unusedDependencyWarning(dependency: String, packageFile: String, line: Int? = nil) -> String {
3838
return warning(
3939
file: packageFile,
40+
line: line,
4041
message: "Unused dependency '\(dependency)' is declared but never imported"
4142
)
4243
}

Sources/SwiftDependencyAuditLib/Models.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
11
import Foundation
22

3+
public struct DependencyInfo: Sendable {
4+
public let name: String
5+
public let lineNumber: Int?
6+
7+
public init(name: String, lineNumber: Int? = nil) {
8+
self.name = name
9+
self.lineNumber = lineNumber
10+
}
11+
}
12+
313
public struct Target: Sendable {
414
public let name: String
515
public let type: TargetType
616
public let dependencies: [String]
17+
public let dependencyInfo: [DependencyInfo]
718
public let path: String?
819

920
public init(name: String, type: TargetType, dependencies: [String], path: String?) {
1021
self.name = name
1122
self.type = type
1223
self.dependencies = dependencies
24+
self.dependencyInfo = dependencies.map { DependencyInfo(name: $0) }
25+
self.path = path
26+
}
27+
28+
public init(name: String, type: TargetType, dependencyInfo: [DependencyInfo], path: String?) {
29+
self.name = name
30+
self.type = type
31+
self.dependencies = dependencyInfo.map { $0.name }
32+
self.dependencyInfo = dependencyInfo
1333
self.path = path
1434
}
1535

Sources/SwiftDependencyAuditLib/PackageParser.swift

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public actor PackageParser {
3333
let dependencies = try extractDependencies(from: content)
3434

3535
// Extract targets
36-
let targets = try extractTargets(from: content)
36+
let targets = try extractTargets(from: content, packageContent: content)
3737

3838
return PackageInfo(
3939
name: packageName,
@@ -89,7 +89,7 @@ public actor PackageParser {
8989
return dependencies
9090
}
9191

92-
private func extractTargets(from content: String) throws -> [Target] {
92+
private func extractTargets(from content: String, packageContent: String) throws -> [Target] {
9393
var targets: [Target] = []
9494

9595
// Try to extract targets from both patterns:
@@ -117,12 +117,12 @@ public actor PackageParser {
117117
for match in targetsSection.matches(of: executableRegex) {
118118
let name = String(match.1)
119119
let dependenciesStr = String(match.2)
120-
let dependencies = parseDependencyList(dependenciesStr)
120+
let dependencyInfo = parseDependencyListWithLineNumbers(dependenciesStr, targetName: name, packageContent: packageContent)
121121

122122
targets.append(Target(
123123
name: name,
124124
type: .executable,
125-
dependencies: dependencies,
125+
dependencyInfo: dependencyInfo,
126126
path: nil
127127
))
128128
}
@@ -131,12 +131,12 @@ public actor PackageParser {
131131
for match in targetsSection.matches(of: libraryRegex) {
132132
let name = String(match.1)
133133
let dependenciesStr = String(match.2)
134-
let dependencies = parseDependencyList(dependenciesStr)
134+
let dependencyInfo = parseDependencyListWithLineNumbers(dependenciesStr, targetName: name, packageContent: packageContent)
135135

136136
targets.append(Target(
137137
name: name,
138138
type: .library,
139-
dependencies: dependencies,
139+
dependencyInfo: dependencyInfo,
140140
path: nil
141141
))
142142
}
@@ -145,12 +145,12 @@ public actor PackageParser {
145145
for match in targetsSection.matches(of: testRegex) {
146146
let name = String(match.1)
147147
let dependenciesStr = String(match.2)
148-
let dependencies = parseDependencyList(dependenciesStr)
148+
let dependencyInfo = parseDependencyListWithLineNumbers(dependenciesStr, targetName: name, packageContent: packageContent)
149149

150150
targets.append(Target(
151151
name: name,
152152
type: .test,
153-
dependencies: dependencies,
153+
dependencyInfo: dependencyInfo,
154154
path: nil
155155
))
156156
}
@@ -159,12 +159,12 @@ public actor PackageParser {
159159
for match in targetsSection.matches(of: macroRegex) {
160160
let name = String(match.1)
161161
let dependenciesStr = String(match.2)
162-
let dependencies = parseDependencyList(dependenciesStr)
162+
let dependencyInfo = parseDependencyListWithLineNumbers(dependenciesStr, targetName: name, packageContent: packageContent)
163163

164164
targets.append(Target(
165165
name: name,
166166
type: .library, // Treat macros as library targets for dependency analysis
167-
dependencies: dependencies,
167+
dependencyInfo: dependencyInfo,
168168
path: nil
169169
))
170170
}
@@ -197,12 +197,12 @@ public actor PackageParser {
197197
for match in targetsSection.matches(of: pluginRegex) {
198198
let name = String(match.1)
199199
let dependenciesStr = String(match.2)
200-
let dependencies = parseDependencyList(dependenciesStr)
200+
let dependencyInfo = parseDependencyListWithLineNumbers(dependenciesStr, targetName: name, packageContent: packageContent)
201201

202202
targets.append(Target(
203203
name: name,
204204
type: .plugin,
205-
dependencies: dependencies,
205+
dependencyInfo: dependencyInfo,
206206
path: nil
207207
))
208208
}
@@ -383,6 +383,79 @@ public actor PackageParser {
383383
return dependencies
384384
}
385385

386+
private func parseDependencyListWithLineNumbers(_ dependenciesStr: String, targetName: String, packageContent: String) -> [DependencyInfo] {
387+
var dependencyInfos: [DependencyInfo] = []
388+
389+
// Get the line numbers for dependencies by searching in the full package content
390+
let dependencies = parseDependencyList(dependenciesStr)
391+
392+
for dependency in dependencies {
393+
if let lineNumber = findDependencyLineNumber(dependency: dependency, targetName: targetName, in: packageContent) {
394+
dependencyInfos.append(DependencyInfo(name: dependency, lineNumber: lineNumber))
395+
} else {
396+
dependencyInfos.append(DependencyInfo(name: dependency, lineNumber: nil))
397+
}
398+
}
399+
400+
return dependencyInfos
401+
}
402+
403+
private func findDependencyLineNumber(dependency: String, targetName: String, in content: String) -> Int? {
404+
let lines = content.components(separatedBy: .newlines)
405+
406+
// Find the target declaration first
407+
var targetStartLine: Int?
408+
var targetEndLine: Int?
409+
var braceDepth = 0
410+
var inTarget = false
411+
412+
for (index, line) in lines.enumerated() {
413+
if line.contains("name: \"\(targetName)\"") && (line.contains(".target") || line.contains(".executableTarget") || line.contains(".testTarget") || line.contains(".macro") || line.contains(".plugin")) {
414+
targetStartLine = index + 1
415+
inTarget = true
416+
braceDepth = 0
417+
}
418+
419+
if inTarget {
420+
// Count braces to determine where the target declaration ends
421+
for char in line {
422+
if char == "(" {
423+
braceDepth += 1
424+
} else if char == ")" {
425+
braceDepth -= 1
426+
if braceDepth == 0 {
427+
targetEndLine = index + 1
428+
break
429+
}
430+
}
431+
}
432+
433+
if let targetEndLine = targetEndLine {
434+
break
435+
}
436+
}
437+
}
438+
439+
// Search for the dependency within the target declaration
440+
if let startLine = targetStartLine, let endLine = targetEndLine {
441+
for lineIndex in (startLine - 1)..<min(endLine, lines.count) {
442+
let line = lines[lineIndex]
443+
// Look for the dependency name in quotes or as a product name
444+
if line.contains("\"\(dependency)\"") {
445+
return lineIndex + 1
446+
}
447+
}
448+
}
449+
450+
return nil
451+
}
452+
453+
private func findLineNumber(for range: Range<String.Index>, in content: String) -> Int? {
454+
let prefix = content[..<range.lowerBound]
455+
let lineNumber = prefix.components(separatedBy: .newlines).count
456+
return lineNumber
457+
}
458+
386459
private func repoNameToModuleName(_ repoName: String) -> String {
387460
// Convert common repository naming patterns to module names
388461
let cleanedName = repoName

Sources/SwiftDependencyAuditLib/XcodeOutput.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ public struct XcodeOutput {
3434
)
3535
}
3636

37-
public static func unusedDependencyWarning(dependency: String, packageFile: String) -> String {
37+
public static func unusedDependencyWarning(dependency: String, packageFile: String, line: Int? = nil) -> String {
3838
return warning(
3939
file: packageFile,
40+
line: line,
4041
message: "Unused dependency '\(dependency)' is declared but never imported"
4142
)
4243
}

0 commit comments

Comments
 (0)