Skip to content

Commit 01f85fe

Browse files
[PackageLoading] diagnose empty manifest separately from missing tools version specification (#5893)
This change makes it clearer to the user what problem SwiftPM thinks their manifest has, avoiding some confusions in [cases like this](https://forums.swift.org/t/missing-swift-tools-version-specification-build-fail/61051). In the future, for errors not specific to the tools version specification, we should move their diagnostics to before calling `ToolsVersionParser.parse`. This probably requires some restructuring of manifest parsing in general. Some additional changes: - Removed an incorrect FIXME, and added a TODO for testing `ManifestParseError.runtimeManifestErrors`. - Decapitalised the description for the other `ManifestParseError` cases, in keeping with [Swift’s diagnostics style](https://github.com/apple/swift/blob/3a939255c8fc1f2825560cca3a5fa6b167c414c3/docs/Diagnostics.md).
1 parent 9ec8992 commit 01f85fe

File tree

4 files changed

+57
-12
lines changed

4 files changed

+57
-12
lines changed

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import TSCBasic
1717
import enum TSCUtility.Diagnostics
1818

1919
public enum ManifestParseError: Swift.Error, Equatable {
20+
/// The manifest is empty, or at least from SwiftPM's perspective it is.
21+
case emptyManifest(path: AbsolutePath)
2022
/// The manifest contains invalid format.
2123
case invalidManifestFormat(String, diagnosticFile: AbsolutePath?)
22-
24+
// TODO: Test this error.
2325
/// The manifest was successfully loaded by swift interpreter but there were runtime issues.
2426
case runtimeManifestErrors([String])
2527
}
@@ -28,10 +30,12 @@ public enum ManifestParseError: Swift.Error, Equatable {
2830
extension ManifestParseError: CustomStringConvertible {
2931
public var description: String {
3032
switch self {
33+
case .emptyManifest(let manifestPath):
34+
return "'\(manifestPath)' is empty"
3135
case .invalidManifestFormat(let error, _):
32-
return "Invalid manifest\n\(error)"
36+
return "invalid manifest\n\(error)"
3337
case .runtimeManifestErrors(let errors):
34-
return "Invalid manifest (evaluation failed)\n\(errors.joined(separator: "\n"))"
38+
return "invalid manifest (evaluation failed)\n\(errors.joined(separator: "\n"))"
3539
}
3640
}
3741
}

Sources/PackageLoading/ToolsVersionParser.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,17 @@ public struct ToolsVersionParser {
2121
private init() {}
2222

2323
public static func parse(manifestPath: AbsolutePath, fileSystem: FileSystem) throws -> ToolsVersion {
24-
// FIXME: We don't need the entire file, just the first line.
24+
// FIXME: We should diagnose errors not specific to the tools version specification outside of this function.
25+
// In order to that, maybe we can restructure the parsing to somthing like this:
26+
// parse(_ manifestContent: String) throws -> Manifest {
27+
// ...
28+
// guard !manifestContent.isEmpty else { throw appropariteError }
29+
// let (toolsVersion, remainingContent) = parseAndConsumeToolsVersionSpecification(manifestConent)
30+
// let packageDetails = parsePackageDetails(remainingContent)
31+
// ...
32+
// return Manifest(toolsVersion, ...)
33+
// }
34+
2535
let manifestContents: ByteString
2636
do {
2737
manifestContents = try fileSystem.readFileContents(manifestPath)
@@ -37,6 +47,10 @@ public struct ToolsVersionParser {
3747
throw Error.nonUTF8EncodedManifest(path: manifestPath)
3848
}
3949

50+
guard !manifestContentsDecodedWithUTF8.isEmpty else {
51+
throw ManifestParseError.emptyManifest(path: manifestPath)
52+
}
53+
4054
do {
4155
return try self.parse(utf8String: manifestContentsDecodedWithUTF8)
4256
} catch Error.malformedToolsVersionSpecification(.commentMarker(.isMissing)) {
@@ -45,6 +59,7 @@ public struct ToolsVersionParser {
4559
}
4660

4761
public static func parse(utf8String: String) throws -> ToolsVersion {
62+
assert(!utf8String.isEmpty, "empty manifest should've been diagnosed before parsing the tools version specification")
4863
/// The manifest represented in its constituent parts.
4964
let manifestComponents = Self.split(utf8String)
5065
/// The Swift tools version specification represented in its constituent parts.
@@ -427,7 +442,7 @@ extension ToolsVersionParser {
427442
public enum ToolsVersionSpecificationMalformationLocation {
428443
/// The nature of malformation at the location in Swift tools version specification.
429444
public enum MalformationDetails {
430-
/// The Swift tools version specification component is missing.
445+
/// The Swift tools version specification component is missing in the non-empty manifest.
431446
case isMissing
432447
/// The Swift tools version specification component is misspelt.
433448
case isMisspelt(_ misspelling: String)

Tests/PackageLoadingTests/ToolsVersionParserTests.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ class ToolsVersionParserTests: XCTestCase {
2424
body?(toolsVersion)
2525
}
2626

27+
/// Verifies correct parsing for valid version specifications, and that the parser isn't confused by contents following the version specification.
2728
func testValidVersions() throws {
28-
let validVersions = [
29+
let manifestsSnippetWithValidVersionSpecification = [
2930
// No spacing surrounding the label for Swift ≥ 5.4:
3031
"//swift-tools-version:5.4.0" : (5, 4, 0, "5.4.0"),
3132
"//swift-tools-version:5.4-dev" : (5, 4, 0, "5.4.0"),
@@ -105,8 +106,8 @@ class ToolsVersionParserTests: XCTestCase {
105106
"\u{D}\u{A}\t\u{85}\u{85}\u{A}\u{2028}\u{2029}//\u{2001}\u{2002}\u{202F}sWiFt-tOoLs-vErSiOn:\u{1680}\t\u{2000}\u{200A} \u{2002}\u{202F}5.5.2\nkkk\n" : (5, 5, 2, "5.5.2"),
106107
]
107108

108-
for (version, result) in validVersions {
109-
try self.parse(version) { toolsVersion in
109+
for (snippet, result) in manifestsSnippetWithValidVersionSpecification {
110+
try self.parse(snippet) { toolsVersion in
110111
XCTAssertEqual(toolsVersion.major, result.0)
111112
XCTAssertEqual(toolsVersion.minor, result.1)
112113
XCTAssertEqual(toolsVersion.patch, result.2)
@@ -135,11 +136,37 @@ class ToolsVersionParserTests: XCTestCase {
135136
}
136137
}
137138

138-
/// Verifies that the correct error is thrown for each manifest missing its Swift tools version specification.
139+
/// Verifies that if a manifest appears empty to SwiftPM, a distinct error is thrown.
140+
func testEmptyManifest() throws {
141+
let fs = InMemoryFileSystem()
142+
143+
let packageRoot = AbsolutePath(path: "/lorem/ipsum/dolor")
144+
try fs.createDirectory(packageRoot, recursive: true)
145+
146+
let manifestPath = packageRoot.appending(component: "Package.swift")
147+
try fs.writeFileContents(manifestPath, bytes: "")
148+
149+
XCTAssertThrowsError(
150+
try ToolsVersionParser.parse(manifestPath: manifestPath, fileSystem: fs),
151+
"empty manifest '/lorem/ipsum/dolor/Package.swift'") { error in
152+
guard let error = error as? ManifestParseError, case .emptyManifest(let errorPath) = error else {
153+
XCTFail("'ManifestParseError.emptyManifest' should've been thrown, but a different error is thrown")
154+
return
155+
}
156+
157+
guard errorPath == manifestPath else {
158+
XCTFail("error is in '\(manifestPath)', but '\(errorPath)' is given for the error message")
159+
return
160+
}
161+
162+
XCTAssertEqual(error.description, "'/lorem/ipsum/dolor/Package.swift' is empty")
163+
}
164+
}
165+
166+
/// Verifies that the correct error is thrown for each non-empty manifest missing its Swift tools version specification.
139167
func testMissingSpecifications() throws {
140168
/// Leading snippets of manifest files that don't have Swift tools version specifications.
141169
let manifestSnippetsWithoutSpecification = [
142-
"",
143170
"\n",
144171
"\n\r\r\n",
145172
"ni",
@@ -185,7 +212,6 @@ class ToolsVersionParserTests: XCTestCase {
185212
"\n\n\nswift-tools-version:3.1\r",
186213
"\r\n\r\ncontrafibularity",
187214
"\n\r\t3.14",
188-
"",
189215
]
190216

191217
for manifestSnippet in manifestSnippetsWithoutSpecificationCommentMarker {

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ final class WorkspaceTests: XCTestCase {
215215
XCTAssert(rootManifests.count == 0, "\(rootManifests)")
216216

217217
testDiagnostics(observability.diagnostics) { result in
218-
let diagnostic = result.check(diagnostic: .prefix("Invalid manifest\n\(path.appending(components: "MyPkg", "Package.swift")):3:8: error: An error in MyPkg"), severity: .error)
218+
let diagnostic = result.check(diagnostic: .prefix("invalid manifest\n\(path.appending(components: "MyPkg", "Package.swift")):3:8: error: An error in MyPkg"), severity: .error)
219219
XCTAssertEqual(diagnostic?.metadata?.packageIdentity, .init(path: pkgDir))
220220
XCTAssertEqual(diagnostic?.metadata?.packageKind, .root(pkgDir))
221221
}

0 commit comments

Comments
 (0)