diff --git a/evaluate.py b/evaluate.py index 9d11f8f..d3831f0 100644 --- a/evaluate.py +++ b/evaluate.py @@ -718,9 +718,10 @@ def evaluate_interface_modules(files: list[SourceFile]) -> list[Violation]: def evaluate_shell_modules(files: list[SourceFile]) -> list[Violation]: core_handler_references_by_domain: dict[str, set[str]] = {} for source_file in files: - if source_file.metadata.module_type == "core": + if source_file.metadata.module_type == "core" and source_file.module_name: core_handler_references_by_domain.setdefault(source_file.metadata.domain, set()).update( - source_file.decision_surface | source_file.decision_products + f"{source_file.module_name}.{reference}" + for reference in source_file.decision_surface | source_file.decision_products ) violations: list[Violation] = [] @@ -728,7 +729,7 @@ def evaluate_shell_modules(files: list[SourceFile]) -> list[Violation]: if source_file.metadata.module_type != "shell": continue core_references = core_handler_references_by_domain.get(source_file.metadata.domain, set()) - if not source_file.api_references.intersection(core_references): + if not source_file.qualified_references.intersection(core_references): violations.append(Violation(source_file.path, "shell module must reference a core API in the same @archlint.domain")) return violations diff --git a/evaluate_test.py b/evaluate_test.py index 1850747..dc6b9e3 100644 --- a/evaluate_test.py +++ b/evaluate_test.py @@ -336,6 +336,7 @@ def test_shell_module_does_not_accept_arbitrary_core_type_reference(self): path="/repo/Handler.swift", metadata={"moduleType": "shell", "domain": "mail.sync", "exemptReason": ""}, apiReferences=["CoreVocabulary"], + qualifiedReferences=["Core.CoreVocabulary"], ) violations = evaluate.evaluate([core, shell]) @@ -359,6 +360,7 @@ def test_shell_module_accepts_core_decision_product_reference(self): path="/repo/Handler.swift", metadata={"moduleType": "shell", "domain": "mail.sync", "exemptReason": ""}, apiReferences=["SyncPlan"], + qualifiedReferences=["Core.SyncPlan"], identifiers=["EffectType"], effectfulIdentifiers=["EffectType"], decisionSurface=[], @@ -382,6 +384,50 @@ def test_shell_module_accepts_core_decision_product_reference(self): self.assertEqual([], violations) + def test_shell_module_does_not_accept_same_named_bare_core_reference(self): + core = source_file(path="/repo/Core.swift") + shell = source_file( + path="/repo/Handler.swift", + metadata={"moduleType": "shell", "domain": "mail.sync", "exemptReason": ""}, + identifiers=["decideSync", "EffectType"], + apiReferences=["decideSync"], + qualifiedReferences=[], + effectfulIdentifiers=["EffectType"], + ) + + violations = evaluate.evaluate([core, shell]) + + self.assertIn( + "shell module must reference a core API in the same @archlint.domain", + [violation.message for violation in violations], + ) + + def test_shell_module_accepts_qualified_core_decision_reference(self): + core = source_file(path="/repo/Core.swift") + shell = source_file( + path="/repo/Handler.swift", + metadata={"moduleType": "shell", "domain": "mail.sync", "exemptReason": ""}, + identifiers=["decideSync", "EffectType"], + apiReferences=["decideSync"], + qualifiedReferences=["Core.decideSync"], + effectfulIdentifiers=["EffectType"], + ) + test = source_file( + path="/repo/CoreTests.swift", + testScope="CoreTests", + metadata={"moduleType": "test", "domain": "mail.sync", "exemptReason": ""}, + apiReferences=["decideSync"], + propertyChecks=[property_check(["decideSync"])], + decisionSurface=[], + propertyTestSurface=[], + decisionProducts=[], + decisionReferences=[], + ) + + violations = evaluate.evaluate([core, shell, test]) + + self.assertEqual([], violations) + def test_shell_module_must_touch_effectful_api(self): core = source_file( path="/repo/Core.swift", diff --git a/go/main_test.go b/go/main_test.go index 6eb5ea5..c8eb8eb 100644 --- a/go/main_test.go +++ b/go/main_test.go @@ -53,17 +53,17 @@ func parseViolationOutput(output string) []violation { func TestLintBackendArchitectureAcceptsHandlerWithDecisionModule(t *testing.T) { backendRoot := newBackendFixture(t, map[string]string{ - "internal/auth/auth_decision.go": `// @archlint.module core + "internal/auth/decision/auth_decision.go": `// @archlint.module core // @archlint.domain auth -package auth +package decision func DecideTokenClaims(subject string) bool { return subject != "" } `, - "internal/auth/auth_decision_test.go": `// @archlint.module test + "internal/auth/decision/auth_decision_test.go": `// @archlint.module test // @archlint.domain auth -package auth +package decision import ( "testing" @@ -83,10 +83,13 @@ func TestDecideTokenClaimsProperty(t *testing.T) { // @archlint.domain auth package auth -import "net/http" +import ( + "archlintfixture/internal/auth/decision" + "net/http" +) func Handle() string { - if DecideTokenClaims(http.MethodGet) { + if decision.DecideTokenClaims(http.MethodGet) { return http.MethodGet } return "" @@ -344,9 +347,9 @@ func Route(_ CoreVocabulary) string { func TestLintBackendArchitectureAcceptsHandlerWithCoreDecisionProductReference(t *testing.T) { backendRoot := newBackendFixture(t, map[string]string{ - "internal/httpapi/http_decision.go": `// @archlint.module core + "internal/httpapi/decision/http_decision.go": `// @archlint.module core // @archlint.domain http-api -package httpapi +package decision type RoutePlan struct{} @@ -357,9 +360,9 @@ func DecideRoute(input bool) RoutePlan { return RoutePlan{} } `, - "internal/httpapi/http_decision_test.go": `// @archlint.module test + "internal/httpapi/decision/http_decision_test.go": `// @archlint.module test // @archlint.domain http-api -package httpapi +package decision import ( "testing" @@ -380,9 +383,12 @@ func TestDecideRouteProperty(t *testing.T) { // @archlint.domain http-api package httpapi -import "net/http" +import ( + "archlintfixture/internal/httpapi/decision" + "net/http" +) -func Route(_ RoutePlan) string { +func Route(_ decision.RoutePlan) string { return http.MethodGet } `, diff --git a/swift/Sources/SwiftArchLint/main.swift b/swift/Sources/SwiftArchLint/main.swift index 5fb9dcf..0a45ee7 100644 --- a/swift/Sources/SwiftArchLint/main.swift +++ b/swift/Sources/SwiftArchLint/main.swift @@ -411,7 +411,7 @@ enum SwiftArchLint { } ) for occurrence: SymbolOccurrence in occurrences { - guard occurrence.roles.contains(.reference), + guard (occurrence.roles.contains(.reference) || occurrence.roles.contains(.call)), occurrence.symbol.language == .swift, !occurrence.location.isSystem else { diff --git a/swift/test.sh b/swift/test.sh index 5c1b9f1..1dd7f0d 100644 --- a/swift/test.sh +++ b/swift/test.sh @@ -672,12 +672,12 @@ import Foundation struct HTTPMailBackendClient { func add() -> URLRequest { - _ = HTTPMailBackendDecider.decide() return URLRequest(url: URL(string: "http://localhost")!) } } EOF -assert_passes "$core_enum_case_matching_shell_method_fixture" +assert_fails_with "$core_enum_case_matching_shell_method_fixture" \ + "shell module must reference a core API in the same @archlint.domain" empty_decider_fixture="$(new_fixture empty-decider)" cat > "$empty_decider_fixture/apps/ios/MailApp/Backend/SQLiteMailSyncStateDecider.swift" <<'EOF'