Skip to content

Commit 0a7a30b

Browse files
committed
consolidate ad improve manifest validation
motivation: make code easier to reason about and maintain changes: * create new ManfeistValidator utility * move manifest validation code out of ManifestLoader and Workspace and into the new validation utility * seperate out manifst validation diagnostics from manifest loading diagnostics such that a fake diagnostics scope is no longer required * remove a couple of redundent manifest validation tests since a more robust validation is happening when loding the graph * adjust call sites and tests
1 parent d47f2e4 commit 0a7a30b

17 files changed

+561
-530
lines changed

Sources/Basics/Observability.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ public protocol DiagnosticsEmitterProtocol {
160160
}
161161

162162
extension DiagnosticsEmitterProtocol {
163+
public func emit(_ diagnostics: [Diagnostic]) {
164+
for diagnostic in diagnostics {
165+
self.emit(diagnostic)
166+
}
167+
}
168+
163169
public func emit(severity: Diagnostic.Severity, message: String, metadata: ObservabilityMetadata? = .none) {
164170
self.emit(.init(severity: severity, message: message, metadata: metadata))
165171
}
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Tom Doron on 3/4/22.
6+
//
7+
8+
import Basics
9+
import Foundation
10+
import PackageModel
11+
import TSCBasic
12+
13+
public struct ManifestValidator {
14+
static var supportedLocalBinaryDependencyExtensions: [String] {
15+
["zip"] + BinaryTarget.Kind.allCases.filter{ $0 != .unknown }.map { $0.fileExtension }
16+
}
17+
static var supportedRemoteBinaryDependencyExtensions: [String] {
18+
["zip", "artifactbundleindex"]
19+
}
20+
21+
private let manifest: Manifest
22+
private let sourceControlValidator: ManifestSourceControlValidator
23+
private let fileSystem: FileSystem
24+
25+
public init(manifest: Manifest, sourceControlValidator: ManifestSourceControlValidator, fileSystem: FileSystem) {
26+
self.manifest = manifest
27+
self.sourceControlValidator = sourceControlValidator
28+
self.fileSystem = fileSystem
29+
}
30+
31+
/// Validate the provided manifest.
32+
public func validate() -> [Basics.Diagnostic] {
33+
var diagnostics = [Basics.Diagnostic]()
34+
35+
diagnostics += self.validateTargets()
36+
diagnostics += self.validateProducts()
37+
diagnostics += self.validateDependencies()
38+
39+
// Checks reserved for tools version 5.2 features
40+
if self.manifest.toolsVersion >= .v5_2 {
41+
diagnostics += self.validateTargetDependencyReferences()
42+
diagnostics += self.validateBinaryTargets()
43+
}
44+
45+
return diagnostics
46+
}
47+
48+
private func validateTargets() -> [Basics.Diagnostic] {
49+
var diagnostics = [Basics.Diagnostic]()
50+
51+
let duplicateTargetNames = self.manifest.targets.map({ $0.name }).spm_findDuplicates()
52+
for name in duplicateTargetNames {
53+
diagnostics.append(.duplicateTargetName(targetName: name))
54+
}
55+
56+
return diagnostics
57+
}
58+
59+
private func validateProducts() -> [Basics.Diagnostic] {
60+
var diagnostics = [Basics.Diagnostic]()
61+
62+
for product in self.manifest.products {
63+
// Check that the product contains targets.
64+
guard !product.targets.isEmpty else {
65+
diagnostics.append(.emptyProductTargets(productName: product.name))
66+
continue
67+
}
68+
69+
// Check that the product references existing targets.
70+
for target in product.targets {
71+
if !self.manifest.targetMap.keys.contains(target) {
72+
diagnostics.append(.productTargetNotFound(productName: product.name, targetName: target, validTargets: self.manifest.targetMap.keys.sorted()))
73+
}
74+
}
75+
76+
// Check that products that reference only binary targets don't define a type.
77+
let areTargetsBinary = product.targets.allSatisfy { self.manifest.targetMap[$0]?.type == .binary }
78+
if areTargetsBinary && product.type != .library(.automatic) {
79+
diagnostics.append(.invalidBinaryProductType(productName: product.name))
80+
}
81+
}
82+
83+
return diagnostics
84+
}
85+
86+
private func validateDependencies() -> [Basics.Diagnostic] {
87+
var diagnostics = [Basics.Diagnostic]()
88+
89+
// validate dependency requirements
90+
for dependency in self.manifest.dependencies {
91+
switch dependency {
92+
case .sourceControl(let sourceControl):
93+
diagnostics += validateSourceControlDependency(sourceControl)
94+
default:
95+
break
96+
}
97+
}
98+
99+
return diagnostics
100+
}
101+
102+
private func validateBinaryTargets() -> [Basics.Diagnostic] {
103+
var diagnostics = [Basics.Diagnostic]()
104+
105+
// Check that binary targets point to the right file type.
106+
for target in self.manifest.targets where target.type == .binary {
107+
if target.isLocal {
108+
guard let path = target.path else {
109+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
110+
continue
111+
}
112+
113+
guard let path = path.spm_chuzzle(), !path.isEmpty else {
114+
diagnostics.append(.invalidLocalBinaryPath(path: path, targetName: target.name))
115+
continue
116+
}
117+
118+
guard let relativePath = try? RelativePath(validating: path) else {
119+
diagnostics.append(.invalidLocalBinaryPath(path: path, targetName: target.name))
120+
continue
121+
}
122+
123+
let validExtensions = Self.supportedLocalBinaryDependencyExtensions
124+
guard let fileExtension = relativePath.extension, validExtensions.contains(fileExtension) else {
125+
diagnostics.append(.unsupportedBinaryLocationExtension(
126+
targetName: target.name,
127+
validExtensions: validExtensions
128+
))
129+
continue
130+
}
131+
} else if target.isRemote {
132+
guard let url = target.url else {
133+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
134+
continue
135+
}
136+
137+
guard let url = url.spm_chuzzle(), !url.isEmpty else {
138+
diagnostics.append(.invalidBinaryURL(url: url, targetName: target.name))
139+
continue
140+
}
141+
142+
guard let url = URL(string: url) else {
143+
diagnostics.append(.invalidBinaryURL(url: url, targetName: target.name))
144+
continue
145+
}
146+
147+
let validSchemes = ["https"]
148+
guard url.scheme.map({ validSchemes.contains($0) }) ?? false else {
149+
diagnostics.append(.invalidBinaryURLScheme(
150+
targetName: target.name,
151+
validSchemes: validSchemes
152+
))
153+
continue
154+
}
155+
156+
guard Self.supportedRemoteBinaryDependencyExtensions.contains(url.pathExtension) else {
157+
diagnostics.append(.unsupportedBinaryLocationExtension(
158+
targetName: target.name,
159+
validExtensions: Self.supportedRemoteBinaryDependencyExtensions
160+
))
161+
continue
162+
}
163+
164+
} else {
165+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
166+
continue
167+
}
168+
}
169+
170+
return diagnostics
171+
}
172+
173+
/// Validates that product target dependencies reference an existing package.
174+
private func validateTargetDependencyReferences() -> [Basics.Diagnostic] {
175+
var diagnostics = [Basics.Diagnostic]()
176+
177+
for target in self.manifest.targets {
178+
for targetDependency in target.dependencies {
179+
switch targetDependency {
180+
case .target:
181+
// If this is a target dependency, we don't need to check anything.
182+
break
183+
case .product(_, let packageName, _, _):
184+
if self.manifest.packageDependency(referencedBy: targetDependency) == nil {
185+
diagnostics.append(.unknownTargetPackageDependency(
186+
packageName: packageName ?? "unknown package name",
187+
targetName: target.name,
188+
validPackages: self.manifest.dependencies.map { $0.nameForTargetDependencyResolutionOnly }
189+
))
190+
}
191+
case .byName(let name, _):
192+
// Don't diagnose root manifests so we can emit a better diagnostic during package loading.
193+
if !self.manifest.packageKind.isRoot &&
194+
!self.manifest.targetMap.keys.contains(name) &&
195+
self.manifest.packageDependency(referencedBy: targetDependency) == nil
196+
{
197+
diagnostics.append(.unknownTargetDependency(
198+
dependency: name,
199+
targetName: target.name,
200+
validDependencies: self.manifest.dependencies.map { $0.nameForTargetDependencyResolutionOnly }
201+
))
202+
}
203+
}
204+
}
205+
}
206+
207+
return diagnostics
208+
}
209+
210+
func validateSourceControlDependency(_ dependency: PackageDependency.SourceControl) -> [Basics.Diagnostic] {
211+
var diagnostics = [Basics.Diagnostic]()
212+
213+
// validate source control ref
214+
switch dependency.requirement {
215+
case .branch(let name):
216+
if !self.sourceControlValidator.isValidRefFormat(name) {
217+
diagnostics.append(.invalidSourceControlBranchName(name))
218+
}
219+
case .revision(let revision):
220+
if !self.sourceControlValidator.isValidRefFormat(revision) {
221+
diagnostics.append(.invalidSourceControlRevision(revision))
222+
}
223+
default:
224+
break
225+
}
226+
// if a location is on file system, validate it is in fact a git repo
227+
// there is a case to be made to throw early (here) if the path does not exists
228+
// but many of our tests assume they can pass a non existent path
229+
if case .local(let localPath) = dependency.location, self.fileSystem.exists(localPath) {
230+
if !self.sourceControlValidator.isValidDirectory(localPath) {
231+
// Provides better feedback when mistakingly using url: for a dependency that
232+
// is a local package. Still allows for using url with a local package that has
233+
// also been initialized by git
234+
diagnostics.append(.invalidSourceControlDirectory(localPath))
235+
}
236+
}
237+
return diagnostics
238+
}
239+
}
240+
241+
public protocol ManifestSourceControlValidator {
242+
func isValidRefFormat(_ revision: String) -> Bool
243+
func isValidDirectory(_ path: AbsolutePath) -> Bool
244+
}
245+
246+
extension Basics.Diagnostic {
247+
static func duplicateTargetName(targetName: String) -> Self {
248+
.error("duplicate target named '\(targetName)'")
249+
}
250+
251+
static func emptyProductTargets(productName: String) -> Self {
252+
.error("product '\(productName)' doesn't reference any targets")
253+
}
254+
255+
static func productTargetNotFound(productName: String, targetName: String, validTargets: [String]) -> Self {
256+
.error("target '\(targetName)' referenced in product '\(productName)' could not be found; valid targets are: '\(validTargets.joined(separator: "', '"))'")
257+
}
258+
259+
static func invalidBinaryProductType(productName: String) -> Self {
260+
.error("invalid type for binary product '\(productName)'; products referencing only binary targets must have a type of 'library'")
261+
}
262+
263+
/*static func duplicateDependency(dependencyIdentity: PackageIdentity) -> Self {
264+
.error("duplicate dependency '\(dependencyIdentity)'")
265+
}
266+
267+
static func duplicateDependencyName(dependencyName: String) -> Self {
268+
.error("duplicate dependency named '\(dependencyName)'; consider differentiating them using the 'name' argument")
269+
}*/
270+
271+
static func unknownTargetDependency(dependency: String, targetName: String, validDependencies: [String]) -> Self {
272+
.error("unknown dependency '\(dependency)' in target '\(targetName)'; valid dependencies are: '\(validDependencies.joined(separator: "', '"))'")
273+
}
274+
275+
static func unknownTargetPackageDependency(packageName: String, targetName: String, validPackages: [String]) -> Self {
276+
.error("unknown package '\(packageName)' in dependencies of target '\(targetName)'; valid packages are: '\(validPackages.joined(separator: "', '"))'")
277+
}
278+
279+
static func invalidBinaryLocation(targetName: String) -> Self {
280+
.error("invalid location for binary target '\(targetName)'")
281+
}
282+
283+
static func invalidBinaryURL(url: String, targetName: String) -> Self {
284+
.error("invalid URL '\(url)' for binary target '\(targetName)'")
285+
}
286+
287+
static func invalidLocalBinaryPath(path: String, targetName: String) -> Self {
288+
.error("invalid local path '\(path)' for binary target '\(targetName)', path expected to be relative to package root.")
289+
}
290+
291+
static func invalidBinaryURLScheme(targetName: String, validSchemes: [String]) -> Self {
292+
.error("invalid URL scheme for binary target '\(targetName)'; valid schemes are: '\(validSchemes.joined(separator: "', '"))'")
293+
}
294+
295+
static func unsupportedBinaryLocationExtension(targetName: String, validExtensions: [String]) -> Self {
296+
.error("unsupported extension for binary target '\(targetName)'; valid extensions are: '\(validExtensions.joined(separator: "', '"))'")
297+
}
298+
299+
static func invalidLanguageTag(_ languageTag: String) -> Self {
300+
.error("""
301+
invalid language tag '\(languageTag)'; the pattern for language tags is groups of latin characters and \
302+
digits separated by hyphens
303+
""")
304+
}
305+
306+
static func invalidSourceControlBranchName(_ name: String) -> Self {
307+
.error("invalid branch name: '\(name)'")
308+
}
309+
310+
static func invalidSourceControlRevision(_ revision: String) -> Self {
311+
.error("invalid revision: '\(revision)'")
312+
}
313+
314+
static func invalidSourceControlDirectory(_ path: AbsolutePath) -> Self {
315+
.error("cannot clone from local directory \(path)\nPlease git init or use \"path:\" for \(path)")
316+
}
317+
}
318+
319+
extension TargetDescription {
320+
fileprivate var isRemote: Bool { url != nil }
321+
fileprivate var isLocal: Bool { path != nil }
322+
}

0 commit comments

Comments
 (0)