Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,17 +718,18 @@ 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] = []
for source_file in files:
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

Expand Down
46 changes: 46 additions & 0 deletions evaluate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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=[],
Expand All @@ -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",
Expand Down
30 changes: 18 additions & 12 deletions go/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 ""
Expand Down Expand Up @@ -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{}

Expand All @@ -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"
Expand All @@ -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
}
`,
Expand Down
2 changes: 1 addition & 1 deletion swift/Sources/SwiftArchLint/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions swift/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading