Add linting for mismatching build configurations in a workspace (#474)

Resolves https://github.com/tuist/tuist/issues/467

- All projects' build configurations are now validated against the top level projects
- In the event any are missing or are mismatching (e.g. have a mismatching variant) the linter will now flag them

Example:

```
$ tuist generate
The following issues have been found:
  - The project 'Framework2' has missing or mismatching configurations. It has [Debug (debug), Release (release)], other projects have  [Beta (release), Debug (debug), Release (release)].
```

Test Plan:

- Run `tuist generate` within `fixtures/ios_app_with_multi_configs`
- Comment out any of the build configurations defined in `App`, `Framework1` or `Framework2`
- Re-run `tuist generate`
- Verify a lint warning is displayed
This commit is contained in:
Kas 2019-08-04 10:01:57 +01:00 committed by GitHub
parent 673926599b
commit a3684c66d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 1 deletions

View File

@ -14,6 +14,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- Adding ability to re-generate individual projects https://github.com/tuist/tuist/pull/457 by @kwridan
- Support multiple header paths https://github.com/tuist/tuist/pull/459 by @adamkhazi
- Allow specifying multiple configurations within project manifests https://github.com/tuist/tuist/pull/451 by @kwridan
- Add linting for mismatching build configurations in a workspace https://github.com/tuist/tuist/pull/474 by @kwridan
### Fixed

View File

@ -38,6 +38,7 @@ class GraphLinter: GraphLinting {
var issues: [LintingIssue] = []
issues.append(contentsOf: graph.projects.flatMap(projectLinter.lint))
issues.append(contentsOf: lintDependencies(graph: graph))
issues.append(contentsOf: lintMismatchingConfigurations(graph: graph))
return issues
}
@ -148,6 +149,30 @@ class GraphLinter: GraphLinting {
return [issue]
}
private func lintMismatchingConfigurations(graph: Graphing) -> [LintingIssue] {
let entryNodeProjects = graph.entryNodes.compactMap { $0 as? TargetNode }.map { $0.project }
let knownConfigurations = entryNodeProjects.reduce(into: Set()) {
$0.formUnion(Set($1.settings.configurations.keys))
}
let projectBuildConfigurations = graph.projects.map {
(name: $0.name, buildConfigurations: Set($0.settings.configurations.keys))
}
let mismatchingBuildConfigurations = projectBuildConfigurations.filter {
!knownConfigurations.isSubset(of: $0.buildConfigurations)
}
return mismatchingBuildConfigurations.map {
let expectedConfigurations = knownConfigurations.sorted()
let configurations = $0.buildConfigurations.sorted()
let reason = "The project '\($0.name)' has missing or mismatching configurations. It has \(configurations), other projects have \(expectedConfigurations)"
return LintingIssue(reason: reason,
severity: .warning)
}
}
struct LintableTarget: Equatable, Hashable {
let platform: Platform
let product: Product
@ -162,6 +187,7 @@ class GraphLinter: GraphLinting {
LintableTarget(platform: .iOS, product: .staticFramework),
LintableTarget(platform: .iOS, product: .bundle),
// LintableTarget(platform: .iOS, product: .appExtension),
// LintableTarget(platform: .iOS, product: .appExtension),
// LintableTarget(platform: .iOS, product: .messagesExtension),
// LintableTarget(platform: .iOS, product: .stickerPack),
// LintableTarget(platform: .watchOS, product: .watch2App),

View File

@ -54,3 +54,9 @@ extension BuildConfiguration: Comparable {
extension BuildConfiguration: XcodeRepresentable {
public var xcodeValue: String { return name }
}
extension BuildConfiguration: CustomStringConvertible {
public var description: String {
return "\(name) (\(variant.rawValue))"
}
}

View File

@ -45,14 +45,19 @@ extension Graph {
/// The `dependencies` property is used to define the dependencies explicitly.
/// All targets need to be listed even if they don't have any dependencies.
static func create(projects: [Project] = [],
entryNodes: [Target]? = nil,
dependencies: [(project: Project, target: Target, dependencies: [Target])]) -> Graph {
let targetNodes = createTargetNodes(dependencies: dependencies)
let entryNodes = entryNodes.map { entryNodes in
targetNodes.filter { entryNodes.contains($0.target) }
}
let cache = GraphLoaderCache()
let graph = Graph.test(name: projects.first?.name ?? "Test",
entryPath: projects.first?.path ?? "/test/path",
cache: cache,
entryNodes: targetNodes)
entryNodes: entryNodes ?? targetNodes)
targetNodes.forEach { cache.add(targetNode: $0) }
projects.forEach { cache.add(project: $0) }

View File

@ -247,4 +247,119 @@ final class GraphLinterTests: XCTestCase {
// Then
XCTAssertFalse(result.isEmpty)
}
func test_lint_missingProjectConfigurationsFromDependencyProjects() throws {
// Given
let customConfigurations: [BuildConfiguration: Configuration?] = [
.debug("Debug"): nil,
.debug("Testing"): nil,
.release("Beta"): nil,
.release("Release"): nil,
]
let targetA = Target.empty(name: "TargetA", product: .framework)
let projectA = Project.empty(path: "/path/to/a", name: "ProjectA", settings: Settings(configurations: customConfigurations))
let targetB = Target.empty(name: "TargetB", product: .framework)
let projectB = Project.empty(path: "/path/to/b", name: "ProjectB", settings: Settings(configurations: customConfigurations))
let targetC = Target.empty(name: "TargetC", product: .framework)
let projectC = Project.empty(path: "/path/to/c", name: "ProjectC", settings: .default)
let graph = Graph.create(projects: [projectA, projectB, projectC],
dependencies: [
(project: projectA, target: targetA, dependencies: [targetB]),
(project: projectB, target: targetB, dependencies: [targetC]),
(project: projectC, target: targetC, dependencies: []),
])
// When
let result = subject.lint(graph: graph)
// Then
XCTAssertEqual(result, [
LintingIssue(reason: "The project 'ProjectC' has missing or mismatching configurations. It has [Debug (debug), Release (release)], other projects have [Beta (release), Debug (debug), Release (release), Testing (debug)]",
severity: .warning),
])
}
func test_lint_mismatchingProjectConfigurationsFromDependencyProjects() throws {
// Given
let customConfigurations: [BuildConfiguration: Configuration?] = [
.debug("Debug"): nil,
.debug("Testing"): nil,
.release("Beta"): nil,
.release("Release"): nil,
]
let targetA = Target.empty(name: "TargetA", product: .framework)
let projectA = Project.empty(path: "/path/to/a", name: "ProjectA", settings: Settings(configurations: customConfigurations))
let targetB = Target.empty(name: "TargetB", product: .framework)
let projectB = Project.empty(path: "/path/to/b", name: "ProjectB", settings: Settings(configurations: customConfigurations))
let mismatchingConfigurations: [BuildConfiguration: Configuration?] = [
.release("Debug"): nil,
.release("Testing"): nil,
.release("Beta"): nil,
.release("Release"): nil,
]
let targetC = Target.empty(name: "TargetC", product: .framework)
let projectC = Project.empty(path: "/path/to/c", name: "ProjectC", settings: Settings(configurations: mismatchingConfigurations))
let graph = Graph.create(projects: [projectA, projectB, projectC],
entryNodes: [targetA],
dependencies: [
(project: projectA, target: targetA, dependencies: [targetB]),
(project: projectB, target: targetB, dependencies: [targetC]),
(project: projectC, target: targetC, dependencies: []),
])
// When
let result = subject.lint(graph: graph)
// Then
XCTAssertEqual(result, [
LintingIssue(reason: "The project 'ProjectC' has missing or mismatching configurations. It has [Beta (release), Debug (release), Release (release), Testing (release)], other projects have [Beta (release), Debug (debug), Release (release), Testing (debug)]",
severity: .warning),
])
}
func test_lint_doesNotFlagDependenciesWithExtraConfigurations() throws {
// Lower level dependencies could be shared by projects in different workspaces as such
// it is ok for them to contain more configurations than the entry node projects
// Given
let customConfigurations: [BuildConfiguration: Configuration?] = [
.debug("Debug"): nil,
.release("Beta"): nil,
.release("Release"): nil,
]
let targetA = Target.empty(name: "TargetA", product: .framework)
let projectA = Project.empty(path: "/path/to/a", name: "ProjectA", settings: Settings(configurations: customConfigurations))
let targetB = Target.empty(name: "TargetB", product: .framework)
let projectB = Project.empty(path: "/path/to/b", name: "ProjectB", settings: Settings(configurations: customConfigurations))
let additionalConfigurations: [BuildConfiguration: Configuration?] = [
.debug("Debug"): nil,
.debug("Testing"): nil,
.release("Beta"): nil,
.release("Release"): nil,
]
let targetC = Target.empty(name: "TargetC", product: .framework)
let projectC = Project.empty(path: "/path/to/c", name: "ProjectC", settings: Settings(configurations: additionalConfigurations))
let graph = Graph.create(projects: [projectA, projectB, projectC],
entryNodes: [targetA],
dependencies: [
(project: projectA, target: targetA, dependencies: [targetB]),
(project: projectB, target: targetB, dependencies: [targetC]),
(project: projectC, target: targetC, dependencies: []),
])
// When
let result = subject.lint(graph: graph)
// Then
XCTAssertEqual(result, [])
}
}