diff --git a/Package.resolved b/Package.resolved index af95993..e5e92c0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -49,8 +49,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj", "state" : { - "revision" : "a3e5d54f8c8a2964ee54870fda33b28651416581", - "version" : "8.17.0" + "revision" : "e29e0db843062f5157f77d5b68f237eb5aa43f12", + "version" : "8.18.0" } }, { diff --git a/README.md b/README.md index e4f196b..0b4431c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ opt_in_rules: - targets_use_xcconfig # checks for any targets without a XCConfig file set - projects_use_xcconfig # checks for any projects without a XCConfig file set - shared_scheme_skips_tests # checks for any shared schemes that have disabled tests + - shared_schemes # checks that all targets have a shared scheme present # Other rules make sense for all projects by default. You must opt-out of those. disabled_rules: diff --git a/Sources/XCLinting/Rules/HasSharedSchemeRule.swift b/Sources/XCLinting/Rules/HasSharedSchemeRule.swift new file mode 100644 index 0000000..9ddc7c2 --- /dev/null +++ b/Sources/XCLinting/Rules/HasSharedSchemeRule.swift @@ -0,0 +1,37 @@ +import Foundation + +import XcodeProj +import XCConfig + +/// Ensure that targets have a shared scheme on disk. +struct SharedSchemesRule { + func run(_ environment: XCLinter.Environment) throws -> [Violation] { + let sharedSchemesURL = environment + .projectRootURL + .appendingPathComponent("xcshareddata/xcschemes", isDirectory: true) + .standardizedFileURL + + let fileManager = FileManager.default + + guard fileManager.isReadableFile(atPath: sharedSchemesURL.path) else { + return [.init("Shared scheme directory not found")] + } + + let schemes = Set(try fileManager.contentsOfDirectory(atPath: sharedSchemesURL.path)) + + var violations = [Violation]() + + // check targets + environment.project.pbxproj.enumerateBuildConfigurations { name, configList in + let schemeName = name + ".xcscheme" + + if schemes.contains(schemeName) { + return + } + + violations.append(.init("No shared scheme found for \"\(name)\"")) + } + + return violations + } +} diff --git a/Sources/XCLinting/XCLinter.swift b/Sources/XCLinting/XCLinter.swift index a640445..f4e5688 100644 --- a/Sources/XCLinting/XCLinter.swift +++ b/Sources/XCLinting/XCLinter.swift @@ -84,5 +84,6 @@ extension XCLinter { "projects_use_xcconfig": { try ProjectsUseXCConfigRule().run($0) }, "shared_scheme_skips_tests": { try SharedSchemeSkipsTestsRule().run($0) }, "relative_paths": { try RelativePathsRule().run($0) }, + "shared_schemes": { try SharedSchemesRule().run($0) }, ] } diff --git a/Tests/XCLintTests/SharedSchemesRuleTests.swift b/Tests/XCLintTests/SharedSchemesRuleTests.swift new file mode 100644 index 0000000..a64ff3c --- /dev/null +++ b/Tests/XCLintTests/SharedSchemesRuleTests.swift @@ -0,0 +1,38 @@ +import XCTest + +@testable import XCLinting +import XcodeProj + +final class SharedSchemesRuleTests: XCTestCase { + func testProjectWithSharedSchemes() throws { + let url = try Bundle.module.testDataURL(named: "StockMacOSApp.xcodeproj") + + let project = try XcodeProj(pathString: url.path) + + let env = XCLinter.Environment( + project: project, + projectRootURL: url, + configuration: Configuration() + ) + + let violations = try SharedSchemesRule().run(env) + + XCTAssertTrue(violations.isEmpty) + } + + func testProjectWithMissingSharedSchemes() throws { + let url = try Bundle.module.testDataURL(named: "SchemeSkipsTests.xcodeproj") + + let project = try XcodeProj(pathString: url.path) + + let env = XCLinter.Environment( + project: project, + projectRootURL: url, + configuration: Configuration() + ) + + let violations = try SharedSchemesRule().run(env) + + XCTAssertFalse(violations.isEmpty) + } +}