@archpilotlabs/archpilot 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +538 -0
- package/README.md +1 -0
- package/dist/archpilot-cli.js +850 -159
- package/dist/cli/src/archpilot-cli.js +5523 -0
- package/dist/cli/src/archpilot-cli.js.map +1 -0
- package/dist/core/src/adr/adrLoader.js +97 -0
- package/dist/core/src/adr/adrLoader.js.map +1 -0
- package/dist/core/src/adr/adrParser.js +77 -0
- package/dist/core/src/adr/adrParser.js.map +1 -0
- package/dist/core/src/adr/adrTemplate.js +45 -0
- package/dist/core/src/adr/adrTemplate.js.map +1 -0
- package/dist/core/src/adr/adrTypes.js +3 -0
- package/dist/core/src/adr/adrTypes.js.map +1 -0
- package/dist/core/src/adr/adrValidation.js +160 -0
- package/dist/core/src/adr/adrValidation.js.map +1 -0
- package/dist/core/src/adr/index.js +9 -0
- package/dist/core/src/adr/index.js.map +1 -0
- package/dist/core/src/adrManagement.js +174 -0
- package/dist/core/src/adrManagement.js.map +1 -0
- package/dist/core/src/adrRequirementStatus.js +62 -0
- package/dist/core/src/adrRequirementStatus.js.map +1 -0
- package/dist/core/src/adrScaffoldGeneration.js +325 -0
- package/dist/core/src/adrScaffoldGeneration.js.map +1 -0
- package/dist/core/src/apiDesignAnalysis.js +869 -0
- package/dist/core/src/apiDesignAnalysis.js.map +1 -0
- package/dist/core/src/applicationLayeringAnalysis.js +920 -0
- package/dist/core/src/applicationLayeringAnalysis.js.map +1 -0
- package/dist/core/src/applyRulePack.js +117 -0
- package/dist/core/src/applyRulePack.js.map +1 -0
- package/dist/core/src/architectureBaseline.js +271 -0
- package/dist/core/src/architectureBaseline.js.map +1 -0
- package/dist/core/src/architectureComponentDiscoveryDefaults.js +139 -0
- package/dist/core/src/architectureComponentDiscoveryDefaults.js.map +1 -0
- package/dist/core/src/architectureDependencyContext.js +316 -0
- package/dist/core/src/architectureDependencyContext.js.map +1 -0
- package/dist/core/src/architectureDriftDetection.js +96 -0
- package/dist/core/src/architectureDriftDetection.js.map +1 -0
- package/dist/core/src/architectureDriftTimeline.js +221 -0
- package/dist/core/src/architectureDriftTimeline.js.map +1 -0
- package/dist/core/src/architectureGovernanceBaseline.js +469 -0
- package/dist/core/src/architectureGovernanceBaseline.js.map +1 -0
- package/dist/core/src/architectureHealthHistory.js +208 -0
- package/dist/core/src/architectureHealthHistory.js.map +1 -0
- package/dist/core/src/architectureHealthScore.js +657 -0
- package/dist/core/src/architectureHealthScore.js.map +1 -0
- package/dist/core/src/architectureInit.js +826 -0
- package/dist/core/src/architectureInit.js.map +1 -0
- package/dist/core/src/architectureMap.js +727 -0
- package/dist/core/src/architectureMap.js.map +1 -0
- package/dist/core/src/architectureMigrationPreview.js +467 -0
- package/dist/core/src/architectureMigrationPreview.js.map +1 -0
- package/dist/core/src/architectureModelRefresh.js +209 -0
- package/dist/core/src/architectureModelRefresh.js.map +1 -0
- package/dist/core/src/architectureReportWorkflow.js +139 -0
- package/dist/core/src/architectureReportWorkflow.js.map +1 -0
- package/dist/core/src/architectureValidation.js +3879 -0
- package/dist/core/src/architectureValidation.js.map +1 -0
- package/dist/core/src/architectureWorkspaceModel.js +166 -0
- package/dist/core/src/architectureWorkspaceModel.js.map +1 -0
- package/dist/core/src/authorizationTenantAnalysis.js +652 -0
- package/dist/core/src/authorizationTenantAnalysis.js.map +1 -0
- package/dist/core/src/bootstrapArchitectureConfig.js +150 -0
- package/dist/core/src/bootstrapArchitectureConfig.js.map +1 -0
- package/dist/core/src/changeRisk/changeRiskAssessment.js +251 -0
- package/dist/core/src/changeRisk/changeRiskAssessment.js.map +1 -0
- package/dist/core/src/changeRisk/changeRiskTypes.js +3 -0
- package/dist/core/src/changeRisk/changeRiskTypes.js.map +1 -0
- package/dist/core/src/changeRisk/index.js +6 -0
- package/dist/core/src/changeRisk/index.js.map +1 -0
- package/dist/core/src/ci/githubInlineAnnotations.js +245 -0
- package/dist/core/src/ci/githubInlineAnnotations.js.map +1 -0
- package/dist/core/src/ci/githubPrCommentAdapter.js +232 -0
- package/dist/core/src/ci/githubPrCommentAdapter.js.map +1 -0
- package/dist/core/src/ciWorkflowTemplates.js +1042 -0
- package/dist/core/src/ciWorkflowTemplates.js.map +1 -0
- package/dist/core/src/compliance/complianceExportBuilder.js +122 -0
- package/dist/core/src/compliance/complianceExportBuilder.js.map +1 -0
- package/dist/core/src/compliance/complianceExportTypes.js +3 -0
- package/dist/core/src/compliance/complianceExportTypes.js.map +1 -0
- package/dist/core/src/compliance/complianceExportWriter.js +48 -0
- package/dist/core/src/compliance/complianceExportWriter.js.map +1 -0
- package/dist/core/src/compliance/complianceRulePackResolver.js +62 -0
- package/dist/core/src/compliance/complianceRulePackResolver.js.map +1 -0
- package/dist/core/src/config/index.js +17 -0
- package/dist/core/src/config/index.js.map +1 -0
- package/dist/core/src/config/projectConfig.js +224 -0
- package/dist/core/src/config/projectConfig.js.map +1 -0
- package/dist/core/src/config/projectIdentity.js +160 -0
- package/dist/core/src/config/projectIdentity.js.map +1 -0
- package/dist/core/src/dataQueryRiskDetection.js +1203 -0
- package/dist/core/src/dataQueryRiskDetection.js.map +1 -0
- package/dist/core/src/dependencyValidation.js +2557 -0
- package/dist/core/src/dependencyValidation.js.map +1 -0
- package/dist/core/src/discoverAdrs.js +15 -0
- package/dist/core/src/discoverAdrs.js.map +1 -0
- package/dist/core/src/drift/driftComparator.js +49 -0
- package/dist/core/src/drift/driftComparator.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotLoader.js +136 -0
- package/dist/core/src/drift/driftSnapshotLoader.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotTypes.js +3 -0
- package/dist/core/src/drift/driftSnapshotTypes.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotWriter.js +48 -0
- package/dist/core/src/drift/driftSnapshotWriter.js.map +1 -0
- package/dist/core/src/drift/index.js +6 -0
- package/dist/core/src/drift/index.js.map +1 -0
- package/dist/core/src/eventArchitectureAnalysis.js +925 -0
- package/dist/core/src/eventArchitectureAnalysis.js.map +1 -0
- package/dist/core/src/findingTargetResolution.js +117 -0
- package/dist/core/src/findingTargetResolution.js.map +1 -0
- package/dist/core/src/generateArchitectureDiagrams.js +57 -0
- package/dist/core/src/generateArchitectureDiagrams.js.map +1 -0
- package/dist/core/src/generateImpactAnalysis.js +366 -0
- package/dist/core/src/generateImpactAnalysis.js.map +1 -0
- package/dist/core/src/generateOnboardingDocs.js +262 -0
- package/dist/core/src/generateOnboardingDocs.js.map +1 -0
- package/dist/core/src/governance/cloudConnection.js +203 -0
- package/dist/core/src/governance/cloudConnection.js.map +1 -0
- package/dist/core/src/governance/governanceContracts.js +129 -0
- package/dist/core/src/governance/governanceContracts.js.map +1 -0
- package/dist/core/src/governance/governanceGitMetadata.js +78 -0
- package/dist/core/src/governance/governanceGitMetadata.js.map +1 -0
- package/dist/core/src/governance/governanceUploadClient.js +119 -0
- package/dist/core/src/governance/governanceUploadClient.js.map +1 -0
- package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js +155 -0
- package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js.map +1 -0
- package/dist/core/src/governance/governanceUploadPayload.js +439 -0
- package/dist/core/src/governance/governanceUploadPayload.js.map +1 -0
- package/dist/core/src/governance/runGovernanceUpload.js +103 -0
- package/dist/core/src/governance/runGovernanceUpload.js.map +1 -0
- package/dist/core/src/impactAnalysisEngine.js +376 -0
- package/dist/core/src/impactAnalysisEngine.js.map +1 -0
- package/dist/core/src/initializationState.js +99 -0
- package/dist/core/src/initializationState.js.map +1 -0
- package/dist/core/src/managedArtifacts.js +151 -0
- package/dist/core/src/managedArtifacts.js.map +1 -0
- package/dist/core/src/moduleContractValidation.js +266 -0
- package/dist/core/src/moduleContractValidation.js.map +1 -0
- package/dist/core/src/moduleContractsGenerator.js +337 -0
- package/dist/core/src/moduleContractsGenerator.js.map +1 -0
- package/dist/core/src/moduleDiscovery.js +416 -0
- package/dist/core/src/moduleDiscovery.js.map +1 -0
- package/dist/core/src/moduleScope.js +426 -0
- package/dist/core/src/moduleScope.js.map +1 -0
- package/dist/core/src/onboardingGuidance.js +74 -0
- package/dist/core/src/onboardingGuidance.js.map +1 -0
- package/dist/core/src/operationalLogging.js +7 -0
- package/dist/core/src/operationalLogging.js.map +1 -0
- package/dist/core/src/packageVersion.js +52 -0
- package/dist/core/src/packageVersion.js.map +1 -0
- package/dist/core/src/policy/effectivePolicyResolver.js +91 -0
- package/dist/core/src/policy/effectivePolicyResolver.js.map +1 -0
- package/dist/core/src/policy/exceptionLoader.js +285 -0
- package/dist/core/src/policy/exceptionLoader.js.map +1 -0
- package/dist/core/src/policy/remotePolicyLoader.js +227 -0
- package/dist/core/src/policy/remotePolicyLoader.js.map +1 -0
- package/dist/core/src/policy/remotePolicyTypes.js +3 -0
- package/dist/core/src/policy/remotePolicyTypes.js.map +1 -0
- package/dist/core/src/policy/riskPolicyConfig.js +206 -0
- package/dist/core/src/policy/riskPolicyConfig.js.map +1 -0
- package/dist/core/src/policy/riskPolicyEnforcement.js +203 -0
- package/dist/core/src/policy/riskPolicyEnforcement.js.map +1 -0
- package/dist/core/src/policy/riskPolicyExitCode.js +16 -0
- package/dist/core/src/policy/riskPolicyExitCode.js.map +1 -0
- package/dist/core/src/policy/riskPolicyInitializer.js +90 -0
- package/dist/core/src/policy/riskPolicyInitializer.js.map +1 -0
- package/dist/core/src/quickFix/quickFixEngine.js +132 -0
- package/dist/core/src/quickFix/quickFixEngine.js.map +1 -0
- package/dist/core/src/quickFix/quickFixMessaging.js +15 -0
- package/dist/core/src/quickFix/quickFixMessaging.js.map +1 -0
- package/dist/core/src/quickFix/quickFixProviders.js +87 -0
- package/dist/core/src/quickFix/quickFixProviders.js.map +1 -0
- package/dist/core/src/quickFix/quickFixTypes.js +3 -0
- package/dist/core/src/quickFix/quickFixTypes.js.map +1 -0
- package/dist/core/src/remediation/fileHelpers.js +86 -0
- package/dist/core/src/remediation/fileHelpers.js.map +1 -0
- package/dist/core/src/remediation/fixRegistry.js +645 -0
- package/dist/core/src/remediation/fixRegistry.js.map +1 -0
- package/dist/core/src/remediation/index.js +8 -0
- package/dist/core/src/remediation/index.js.map +1 -0
- package/dist/core/src/remediation/remediationEngine.js +207 -0
- package/dist/core/src/remediation/remediationEngine.js.map +1 -0
- package/dist/core/src/reporting/adrReporter.js +59 -0
- package/dist/core/src/reporting/adrReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureBaselineReporter.js +64 -0
- package/dist/core/src/reporting/architectureBaselineReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureDiagramReporter.js +148 -0
- package/dist/core/src/reporting/architectureDiagramReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureDriftTimelineReporter.js +44 -0
- package/dist/core/src/reporting/architectureDriftTimelineReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureHealthHistoryReporter.js +51 -0
- package/dist/core/src/reporting/architectureHealthHistoryReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureMapReporter.js +228 -0
- package/dist/core/src/reporting/architectureMapReporter.js.map +1 -0
- package/dist/core/src/reporting/architecturePrCommentReporter.js +324 -0
- package/dist/core/src/reporting/architecturePrCommentReporter.js.map +1 -0
- package/dist/core/src/reporting/architecturePrReviewReporter.js +389 -0
- package/dist/core/src/reporting/architecturePrReviewReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureReportArtifacts.js +166 -0
- package/dist/core/src/reporting/architectureReportArtifacts.js.map +1 -0
- package/dist/core/src/reporting/architectureReviewReportGenerator.js +382 -0
- package/dist/core/src/reporting/architectureReviewReportGenerator.js.map +1 -0
- package/dist/core/src/reporting/architectureReviewReporter.js +1633 -0
- package/dist/core/src/reporting/architectureReviewReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureRiskAssessment.js +101 -0
- package/dist/core/src/reporting/architectureRiskAssessment.js.map +1 -0
- package/dist/core/src/reporting/architectureScoreBadge.js +177 -0
- package/dist/core/src/reporting/architectureScoreBadge.js.map +1 -0
- package/dist/core/src/reporting/architectureSupplementalReporters.js +124 -0
- package/dist/core/src/reporting/architectureSupplementalReporters.js.map +1 -0
- package/dist/core/src/reporting/changeRiskAssessment.js +6 -0
- package/dist/core/src/reporting/changeRiskAssessment.js.map +1 -0
- package/dist/core/src/reporting/changeRiskTypes.js +3 -0
- package/dist/core/src/reporting/changeRiskTypes.js.map +1 -0
- package/dist/core/src/reporting/impactAnalysisReporter.js +111 -0
- package/dist/core/src/reporting/impactAnalysisReporter.js.map +1 -0
- package/dist/core/src/reporting/onboardingDocsReporter.js +113 -0
- package/dist/core/src/reporting/onboardingDocsReporter.js.map +1 -0
- package/dist/core/src/reporting/rulePackReporter.js +61 -0
- package/dist/core/src/reporting/rulePackReporter.js.map +1 -0
- package/dist/core/src/reporting/safetyControlsDetection.js +343 -0
- package/dist/core/src/reporting/safetyControlsDetection.js.map +1 -0
- package/dist/core/src/resolveRuleAdrs.js +31 -0
- package/dist/core/src/resolveRuleAdrs.js.map +1 -0
- package/dist/core/src/reviewGenerationWorkflow.js +220 -0
- package/dist/core/src/reviewGenerationWorkflow.js.map +1 -0
- package/dist/core/src/rulePacks/index.js +17 -0
- package/dist/core/src/rulePacks/index.js.map +1 -0
- package/dist/core/src/rulePacks/layeredMonolithRulePack.js +45 -0
- package/dist/core/src/rulePacks/layeredMonolithRulePack.js.map +1 -0
- package/dist/core/src/rulePacks/modularMonolithRulePack.js +41 -0
- package/dist/core/src/rulePacks/modularMonolithRulePack.js.map +1 -0
- package/dist/core/src/rulePacks/types.js +3 -0
- package/dist/core/src/rulePacks/types.js.map +1 -0
- package/dist/core/src/setupNextSteps.js +939 -0
- package/dist/core/src/setupNextSteps.js.map +1 -0
- package/dist/core/src/setupRefresh.js +152 -0
- package/dist/core/src/setupRefresh.js.map +1 -0
- package/dist/core/src/smartInit/apiDbHintDetection.js +836 -0
- package/dist/core/src/smartInit/apiDbHintDetection.js.map +1 -0
- package/dist/core/src/smartInit/architectureProposalBuilder.js +1215 -0
- package/dist/core/src/smartInit/architectureProposalBuilder.js.map +1 -0
- package/dist/core/src/smartInit/architectureStyleDetection.js +472 -0
- package/dist/core/src/smartInit/architectureStyleDetection.js.map +1 -0
- package/dist/core/src/smartInit/archpilotIgnore.js +139 -0
- package/dist/core/src/smartInit/archpilotIgnore.js.map +1 -0
- package/dist/core/src/smartInit/componentDetection.js +1084 -0
- package/dist/core/src/smartInit/componentDetection.js.map +1 -0
- package/dist/core/src/smartInit/implementationProfileResolution.js +53 -0
- package/dist/core/src/smartInit/implementationProfileResolution.js.map +1 -0
- package/dist/core/src/smartInit/index.js +31 -0
- package/dist/core/src/smartInit/index.js.map +1 -0
- package/dist/core/src/smartInit/moduleDetection.js +982 -0
- package/dist/core/src/smartInit/moduleDetection.js.map +1 -0
- package/dist/core/src/smartInit/projectKindDetection.js +437 -0
- package/dist/core/src/smartInit/projectKindDetection.js.map +1 -0
- package/dist/core/src/smartInit/repoTopologyDetection.js +470 -0
- package/dist/core/src/smartInit/repoTopologyDetection.js.map +1 -0
- package/dist/core/src/smartInit/runContext.js +112 -0
- package/dist/core/src/smartInit/runContext.js.map +1 -0
- package/dist/core/src/smartInit/smartInitEngine.js +94 -0
- package/dist/core/src/smartInit/smartInitEngine.js.map +1 -0
- package/dist/core/src/smartInit/stackDetection.js +503 -0
- package/dist/core/src/smartInit/stackDetection.js.map +1 -0
- package/dist/core/src/smartInit/toolingHintDetection.js +345 -0
- package/dist/core/src/smartInit/toolingHintDetection.js.map +1 -0
- package/dist/core/src/sourceScopeClassification.js +83 -0
- package/dist/core/src/sourceScopeClassification.js.map +1 -0
- package/dist/core/src/stackAdapters.js +2475 -0
- package/dist/core/src/stackAdapters.js.map +1 -0
- package/dist/core/src/stackMetadata.js +157 -0
- package/dist/core/src/stackMetadata.js.map +1 -0
- package/dist/core/src/suppressions/index.js +3 -0
- package/dist/core/src/suppressions/index.js.map +1 -0
- package/dist/core/src/suppressions/suppressionReporter.js +38 -0
- package/dist/core/src/suppressions/suppressionReporter.js.map +1 -0
- package/dist/core/src/suppressions/suppressionTypes.js +3 -0
- package/dist/core/src/suppressions/suppressionTypes.js.map +1 -0
- package/dist/core/src/suppressions/suppressionValidator.js +119 -0
- package/dist/core/src/suppressions/suppressionValidator.js.map +1 -0
- package/dist/core/src/transactionDomainAnalysis.js +1416 -0
- package/dist/core/src/transactionDomainAnalysis.js.map +1 -0
- package/dist/core/src/validation/changedFilesResolver.js +165 -0
- package/dist/core/src/validation/changedFilesResolver.js.map +1 -0
- package/dist/core/src/validationProfiling.js +33 -0
- package/dist/core/src/validationProfiling.js.map +1 -0
- package/dist/core/src/validationRuleExplanations.js +69 -0
- package/dist/core/src/validationRuleExplanations.js.map +1 -0
- package/dist/core/src/validationSourceInventory.js +200 -0
- package/dist/core/src/validationSourceInventory.js.map +1 -0
- package/dist/core/src/validationSuppressions.js +231 -0
- package/dist/core/src/validationSuppressions.js.map +1 -0
- package/dist/core/src/validationTypeScriptAstCache.js +82 -0
- package/dist/core/src/validationTypeScriptAstCache.js.map +1 -0
- package/dist/core/src/validationWorkflow.js +222 -0
- package/dist/core/src/validationWorkflow.js.map +1 -0
- package/dist/core/src/workspaceRoot.js +68 -0
- package/dist/core/src/workspaceRoot.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,2557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.dependencyRulesTemplate = exports.DependencyRulesConfigError = void 0;
|
|
37
|
+
exports.isParentChildModuleRelationship = isParentChildModuleRelationship;
|
|
38
|
+
exports.readDependencyRulesConfig = readDependencyRulesConfig;
|
|
39
|
+
exports.buildDependencyGraphSummary = buildDependencyGraphSummary;
|
|
40
|
+
exports.validateDependencyContractBoundaries = validateDependencyContractBoundaries;
|
|
41
|
+
exports.validateDependencyBoundaries = validateDependencyBoundaries;
|
|
42
|
+
exports.buildModuleDependencyGraphSnapshot = buildModuleDependencyGraphSnapshot;
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const node_fs_1 = require("node:fs");
|
|
45
|
+
const ts = __importStar(require("typescript"));
|
|
46
|
+
const architectureInit_1 = require("./architectureInit");
|
|
47
|
+
const architectureDependencyContext_1 = require("./architectureDependencyContext");
|
|
48
|
+
const moduleDiscovery_1 = require("./moduleDiscovery");
|
|
49
|
+
const stackAdapters_1 = require("./stackAdapters");
|
|
50
|
+
const sourceScopeClassification_1 = require("./sourceScopeClassification");
|
|
51
|
+
const dependencyRulesConfigFile = '.archpilot/dependency-rules.json';
|
|
52
|
+
class DependencyRulesConfigError extends Error {
|
|
53
|
+
}
|
|
54
|
+
exports.DependencyRulesConfigError = DependencyRulesConfigError;
|
|
55
|
+
exports.dependencyRulesTemplate = `{
|
|
56
|
+
"modules": {
|
|
57
|
+
"billing": {
|
|
58
|
+
"allowedDependencies": ["shared", "identity"],
|
|
59
|
+
"publicEntrypointsOnly": true
|
|
60
|
+
},
|
|
61
|
+
"identity": {
|
|
62
|
+
"allowedDependencies": ["shared"],
|
|
63
|
+
"publicEntrypointsOnly": true
|
|
64
|
+
},
|
|
65
|
+
"shared": {
|
|
66
|
+
"allowedDependencies": []
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
function normalizePath(filePath) {
|
|
72
|
+
return filePath.replace(/\\/g, '/');
|
|
73
|
+
}
|
|
74
|
+
function toSafeContractModuleFileStem(moduleName) {
|
|
75
|
+
return normalizePath(moduleName)
|
|
76
|
+
.split('/')
|
|
77
|
+
.map((segment) => segment.trim())
|
|
78
|
+
.filter((segment) => segment.length > 0)
|
|
79
|
+
.join('.');
|
|
80
|
+
}
|
|
81
|
+
function normalizeAbsolutePath(filePath) {
|
|
82
|
+
return normalizePath(path.resolve(filePath)).toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
85
|
+
const child = normalizeAbsolutePath(childPath);
|
|
86
|
+
const parent = normalizeAbsolutePath(parentPath);
|
|
87
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
88
|
+
}
|
|
89
|
+
function isParentChildModuleRelationship(leftModule, rightModule) {
|
|
90
|
+
if (leftModule === rightModule) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return leftModule.startsWith(`${rightModule}/`) || rightModule.startsWith(`${leftModule}/`);
|
|
94
|
+
}
|
|
95
|
+
function isPeerModuleImport(dependencyImport) {
|
|
96
|
+
return !isParentChildModuleRelationship(dependencyImport.sourceModule, dependencyImport.targetModule);
|
|
97
|
+
}
|
|
98
|
+
function filterPeerModuleImports(imports) {
|
|
99
|
+
return imports.filter(isPeerModuleImport);
|
|
100
|
+
}
|
|
101
|
+
function getComponentModuleRoots(workspaceRoot, architectureContract) {
|
|
102
|
+
const components = architectureContract?.components;
|
|
103
|
+
if (!components) {
|
|
104
|
+
return new Map();
|
|
105
|
+
}
|
|
106
|
+
const rawEntries = Array.isArray(components)
|
|
107
|
+
? components.map((entry) => {
|
|
108
|
+
const typed = entry;
|
|
109
|
+
return [typed.id, entry];
|
|
110
|
+
})
|
|
111
|
+
: Object.entries(components);
|
|
112
|
+
const componentRoots = new Map();
|
|
113
|
+
for (const [key, value] of rawEntries) {
|
|
114
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const typed = value;
|
|
118
|
+
const componentId = typeof typed.id === 'string' && typed.id.trim().length > 0
|
|
119
|
+
? typed.id.trim()
|
|
120
|
+
: typeof key === 'string' && key.trim().length > 0
|
|
121
|
+
? key.trim()
|
|
122
|
+
: undefined;
|
|
123
|
+
const componentPath = typeof typed.path === 'string' && typed.path.trim().length > 0
|
|
124
|
+
? normalizePath(typed.path.trim())
|
|
125
|
+
: undefined;
|
|
126
|
+
if (!componentId || !componentPath || componentPath === '.') {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
componentRoots.set(componentId, path.join(workspaceRoot, ...componentPath.split('/')));
|
|
130
|
+
}
|
|
131
|
+
return new Map([...componentRoots.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
132
|
+
}
|
|
133
|
+
function buildModuleHierarchyEdges(moduleIds) {
|
|
134
|
+
const moduleIdSet = new Set(moduleIds);
|
|
135
|
+
const edges = [];
|
|
136
|
+
for (const moduleId of moduleIds) {
|
|
137
|
+
const parentModule = moduleId
|
|
138
|
+
.split('/')
|
|
139
|
+
.slice(0, -1)
|
|
140
|
+
.map((_, index, segments) => segments.slice(0, segments.length - index).join('/'))
|
|
141
|
+
.find((candidate) => moduleIdSet.has(candidate));
|
|
142
|
+
if (!parentModule) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
edges.push({ parentModule, childModule: moduleId });
|
|
146
|
+
}
|
|
147
|
+
return edges.sort((left, right) => {
|
|
148
|
+
const parentCompare = left.parentModule.localeCompare(right.parentModule);
|
|
149
|
+
if (parentCompare !== 0) {
|
|
150
|
+
return parentCompare;
|
|
151
|
+
}
|
|
152
|
+
return left.childModule.localeCompare(right.childModule);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function buildChildModulesByParent(hierarchyEdges) {
|
|
156
|
+
const byParent = new Map();
|
|
157
|
+
for (const edge of hierarchyEdges) {
|
|
158
|
+
const children = byParent.get(edge.parentModule) ?? new Set();
|
|
159
|
+
children.add(edge.childModule);
|
|
160
|
+
byParent.set(edge.parentModule, children);
|
|
161
|
+
}
|
|
162
|
+
return new Map([...byParent.entries()].map(([parentModule, childModules]) => [
|
|
163
|
+
parentModule,
|
|
164
|
+
sortUnique(childModules),
|
|
165
|
+
]));
|
|
166
|
+
}
|
|
167
|
+
function isIsolatedContractlessHierarchyModule(input) {
|
|
168
|
+
return (!input.hasContract &&
|
|
169
|
+
input.parentModule !== undefined &&
|
|
170
|
+
input.declaredDependencies.length === 0 &&
|
|
171
|
+
input.actualDependencies.length === 0 &&
|
|
172
|
+
input.inboundDeclaredFrom.length === 0 &&
|
|
173
|
+
input.inboundActualFrom.length === 0 &&
|
|
174
|
+
input.unusedDeclaredDependencies.length === 0 &&
|
|
175
|
+
input.undeclaredActualDependencies.length === 0);
|
|
176
|
+
}
|
|
177
|
+
function getModulesRoot(workspaceRoot, modulesRootRelativePath = 'src/modules') {
|
|
178
|
+
return path.join(workspaceRoot, ...modulesRootRelativePath.split('/'));
|
|
179
|
+
}
|
|
180
|
+
function getScriptKind(filePath) {
|
|
181
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
182
|
+
switch (extension) {
|
|
183
|
+
case '.tsx':
|
|
184
|
+
return ts.ScriptKind.TSX;
|
|
185
|
+
case '.jsx':
|
|
186
|
+
return ts.ScriptKind.JSX;
|
|
187
|
+
case '.js':
|
|
188
|
+
return ts.ScriptKind.JS;
|
|
189
|
+
case '.ts':
|
|
190
|
+
default:
|
|
191
|
+
return ts.ScriptKind.TS;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function usesTypeScriptParser(adapter) {
|
|
195
|
+
return (adapter.dependencyExtractionStrategyHint === 'typescript-imports' ||
|
|
196
|
+
adapter.dependencyExtractionStrategyHint === 'js-imports' ||
|
|
197
|
+
adapter.dependencyExtractionStrategyHint === 'generic');
|
|
198
|
+
}
|
|
199
|
+
function usesJavaImportParser(adapter) {
|
|
200
|
+
return adapter.dependencyExtractionStrategyHint === 'java-imports';
|
|
201
|
+
}
|
|
202
|
+
function usesCSharpImportParser(adapter) {
|
|
203
|
+
return adapter.dependencyExtractionStrategyHint === 'csharp-imports';
|
|
204
|
+
}
|
|
205
|
+
function usesPhpImportParser(adapter) {
|
|
206
|
+
return adapter.dependencyExtractionStrategyHint === 'php-imports';
|
|
207
|
+
}
|
|
208
|
+
function usesGoImportParser(adapter) {
|
|
209
|
+
return adapter.dependencyExtractionStrategyHint === 'go-imports';
|
|
210
|
+
}
|
|
211
|
+
function usesPythonImportParser(adapter) {
|
|
212
|
+
return adapter.dependencyExtractionStrategyHint === 'python-imports';
|
|
213
|
+
}
|
|
214
|
+
function supportsDependencyImportParsing(adapter) {
|
|
215
|
+
return (usesTypeScriptParser(adapter) ||
|
|
216
|
+
usesJavaImportParser(adapter) ||
|
|
217
|
+
usesCSharpImportParser(adapter) ||
|
|
218
|
+
usesPhpImportParser(adapter) ||
|
|
219
|
+
usesGoImportParser(adapter) ||
|
|
220
|
+
usesPythonImportParser(adapter));
|
|
221
|
+
}
|
|
222
|
+
async function pathExists(targetPath) {
|
|
223
|
+
try {
|
|
224
|
+
await node_fs_1.promises.access(targetPath);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function readTextFileIfExists(filePath) {
|
|
232
|
+
try {
|
|
233
|
+
return await node_fs_1.promises.readFile(filePath, { encoding: 'utf8' });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function readPackageName(packageJsonPath) {
|
|
240
|
+
const contents = await readTextFileIfExists(packageJsonPath);
|
|
241
|
+
if (!contents) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(contents);
|
|
246
|
+
return typeof parsed.name === 'string' && parsed.name.trim().length > 0
|
|
247
|
+
? parsed.name.trim()
|
|
248
|
+
: undefined;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function collectPackageExportSubpaths(exportsValue) {
|
|
255
|
+
if (typeof exportsValue === 'string') {
|
|
256
|
+
return ['.'];
|
|
257
|
+
}
|
|
258
|
+
if (!exportsValue || typeof exportsValue !== 'object' || Array.isArray(exportsValue)) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
return Object.keys(exportsValue)
|
|
262
|
+
.filter((key) => key === '.' || key.startsWith('./'))
|
|
263
|
+
.sort((left, right) => left.localeCompare(right));
|
|
264
|
+
}
|
|
265
|
+
async function readPackagePublicExportSubpaths(moduleRoot) {
|
|
266
|
+
const contents = await readTextFileIfExists(path.join(moduleRoot, 'package.json'));
|
|
267
|
+
if (!contents) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(contents);
|
|
272
|
+
const exportSubpaths = collectPackageExportSubpaths(parsed.exports);
|
|
273
|
+
if (exportSubpaths.length > 0) {
|
|
274
|
+
return exportSubpaths;
|
|
275
|
+
}
|
|
276
|
+
return typeof parsed.main === 'string' && parsed.main.trim().length > 0 ? ['.'] : [];
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function buildWorkspacePackageAliasMap(workspaceRoot, moduleRoots) {
|
|
283
|
+
const aliases = [];
|
|
284
|
+
for (const [moduleId, moduleRoot] of [...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
285
|
+
const packageName = await readPackageName(path.join(moduleRoot, 'package.json'));
|
|
286
|
+
if (!packageName) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
aliases.push({
|
|
290
|
+
packageName,
|
|
291
|
+
moduleId,
|
|
292
|
+
moduleRoot,
|
|
293
|
+
targetRoot: await resolvePackageAliasSourceRoot(moduleRoot),
|
|
294
|
+
publicExportSubpaths: await readPackagePublicExportSubpaths(moduleRoot),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
aliases.push(...(await buildTsConfigPathAliasMap(workspaceRoot, moduleRoots)));
|
|
298
|
+
return aliases.sort((left, right) => {
|
|
299
|
+
const lengthCompare = right.packageName.length - left.packageName.length;
|
|
300
|
+
if (lengthCompare !== 0) {
|
|
301
|
+
return lengthCompare;
|
|
302
|
+
}
|
|
303
|
+
return left.packageName.localeCompare(right.packageName);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async function resolvePackageAliasSourceRoot(moduleRoot) {
|
|
307
|
+
const sourceRoot = path.join(moduleRoot, 'src');
|
|
308
|
+
try {
|
|
309
|
+
const stats = await node_fs_1.promises.stat(sourceRoot);
|
|
310
|
+
if (stats.isDirectory()) {
|
|
311
|
+
return sourceRoot;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Use the package root when no conventional source folder exists.
|
|
316
|
+
}
|
|
317
|
+
return moduleRoot;
|
|
318
|
+
}
|
|
319
|
+
async function buildTsConfigPathAliasMap(workspaceRoot, moduleRoots) {
|
|
320
|
+
const contents = await readTextFileIfExists(path.join(workspaceRoot, 'tsconfig.json'));
|
|
321
|
+
if (!contents) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
let paths;
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(contents);
|
|
327
|
+
paths = parsed.compilerOptions?.paths;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
if (!paths) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const aliases = [];
|
|
336
|
+
for (const [aliasPattern, targetPatterns] of Object.entries(paths).sort(([left], [right]) => left.localeCompare(right))) {
|
|
337
|
+
if (!Array.isArray(targetPatterns)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const aliasPrefix = aliasPattern.replace(/\/\*$/u, '');
|
|
341
|
+
if (aliasPrefix.length === 0) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
for (const targetPattern of targetPatterns) {
|
|
345
|
+
if (typeof targetPattern !== 'string' || targetPattern.trim().length === 0) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const isWildcard = /\/\*$/u.test(aliasPattern) && /\/\*$/u.test(targetPattern);
|
|
349
|
+
const normalizedTarget = normalizePath(targetPattern).replace(/\/\*$/u, '');
|
|
350
|
+
const absoluteTarget = path.join(workspaceRoot, ...normalizedTarget.split('/'));
|
|
351
|
+
const moduleEntry = [...moduleRoots.entries()]
|
|
352
|
+
.filter(([, moduleRoot]) => isPathInsideOrEqual(absoluteTarget, moduleRoot))
|
|
353
|
+
.sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]))[0];
|
|
354
|
+
if (!moduleEntry) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const [moduleId, moduleRoot] = moduleEntry;
|
|
358
|
+
if (isWildcard) {
|
|
359
|
+
aliases.push({
|
|
360
|
+
packageName: aliasPrefix,
|
|
361
|
+
moduleId,
|
|
362
|
+
moduleRoot,
|
|
363
|
+
targetRoot: absoluteTarget,
|
|
364
|
+
publicExportSubpaths: await readPackagePublicExportSubpaths(moduleRoot),
|
|
365
|
+
});
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
aliases.push({
|
|
369
|
+
packageName: aliasPrefix,
|
|
370
|
+
moduleId,
|
|
371
|
+
moduleRoot,
|
|
372
|
+
targetFile: absoluteTarget,
|
|
373
|
+
publicExportSubpaths: await readPackagePublicExportSubpaths(moduleRoot),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return aliases;
|
|
378
|
+
}
|
|
379
|
+
function validateDependencyModuleRules(moduleName, value) {
|
|
380
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
381
|
+
throw new DependencyRulesConfigError(`Module "${moduleName}" must be an object in ${dependencyRulesConfigFile}.`);
|
|
382
|
+
}
|
|
383
|
+
const typedValue = value;
|
|
384
|
+
const allowedDependenciesRaw = typedValue.allowedDependencies;
|
|
385
|
+
const publicEntrypointsOnlyRaw = typedValue.publicEntrypointsOnly;
|
|
386
|
+
if (allowedDependenciesRaw !== undefined &&
|
|
387
|
+
(!Array.isArray(allowedDependenciesRaw) ||
|
|
388
|
+
allowedDependenciesRaw.some((item) => typeof item !== 'string'))) {
|
|
389
|
+
throw new DependencyRulesConfigError(`Module "${moduleName}" has an invalid allowedDependencies array in ${dependencyRulesConfigFile}.`);
|
|
390
|
+
}
|
|
391
|
+
if (publicEntrypointsOnlyRaw !== undefined &&
|
|
392
|
+
typeof publicEntrypointsOnlyRaw !== 'boolean') {
|
|
393
|
+
throw new DependencyRulesConfigError(`Module "${moduleName}" has an invalid publicEntrypointsOnly flag in ${dependencyRulesConfigFile}.`);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...(allowedDependenciesRaw !== undefined
|
|
397
|
+
? {
|
|
398
|
+
allowedDependencies: [...allowedDependenciesRaw].sort((a, b) => a.localeCompare(b)),
|
|
399
|
+
}
|
|
400
|
+
: {}),
|
|
401
|
+
...(publicEntrypointsOnlyRaw !== undefined
|
|
402
|
+
? { publicEntrypointsOnly: publicEntrypointsOnlyRaw }
|
|
403
|
+
: {}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function readDependencyRulesConfig(workspaceRoot) {
|
|
407
|
+
const configPath = path.join(workspaceRoot, '.archpilot', 'dependency-rules.json');
|
|
408
|
+
const contents = await readTextFileIfExists(configPath);
|
|
409
|
+
if (!contents) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = JSON.parse(contents);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
throw new DependencyRulesConfigError('ArchPilot dependency rules config is invalid JSON. Please fix .archpilot/dependency-rules.json. Continuing without dependency boundary validation.');
|
|
418
|
+
}
|
|
419
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
420
|
+
throw new DependencyRulesConfigError('ArchPilot dependency rules config must be a JSON object. Please fix .archpilot/dependency-rules.json. Continuing without dependency boundary validation.');
|
|
421
|
+
}
|
|
422
|
+
const modulesRaw = parsed.modules;
|
|
423
|
+
if (!modulesRaw || typeof modulesRaw !== 'object' || Array.isArray(modulesRaw)) {
|
|
424
|
+
throw new DependencyRulesConfigError('ArchPilot dependency rules config must define a "modules" object. Please fix .archpilot/dependency-rules.json. Continuing without dependency boundary validation.');
|
|
425
|
+
}
|
|
426
|
+
const modules = Object.entries(modulesRaw)
|
|
427
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
428
|
+
.reduce((acc, [moduleName, value]) => {
|
|
429
|
+
acc[moduleName] = validateDependencyModuleRules(moduleName, value);
|
|
430
|
+
return acc;
|
|
431
|
+
}, {});
|
|
432
|
+
return { modules };
|
|
433
|
+
}
|
|
434
|
+
async function findModuleRoots(workspaceRoot, modulesRootRelativePath = 'src/modules', architectureContract, options) {
|
|
435
|
+
const discoveredModuleRoots = await (0, moduleDiscovery_1.discoverModuleRoots)(workspaceRoot, modulesRootRelativePath);
|
|
436
|
+
const moduleRoots = new Map();
|
|
437
|
+
const registryEntries = architectureContract?.modules
|
|
438
|
+
? Object.entries(architectureContract.modules).sort(([left], [right]) => left.localeCompare(right))
|
|
439
|
+
: [];
|
|
440
|
+
const contractCache = new Map();
|
|
441
|
+
const candidatesByRoot = new Map();
|
|
442
|
+
for (const [moduleName, moduleReference] of registryEntries) {
|
|
443
|
+
const configuredPath = moduleReference.path;
|
|
444
|
+
if (!configuredPath || typeof configuredPath !== 'string') {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const normalizedConfiguredPath = normalizePath(configuredPath);
|
|
448
|
+
if (/\.[a-z0-9]+$/iu.test(normalizedConfiguredPath)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const absolutePath = path.join(workspaceRoot, ...normalizedConfiguredPath.split('/'));
|
|
452
|
+
try {
|
|
453
|
+
const stats = await node_fs_1.promises.stat(absolutePath);
|
|
454
|
+
if (stats.isDirectory()) {
|
|
455
|
+
const normalizedRoot = normalizeAbsolutePath(absolutePath);
|
|
456
|
+
const existing = candidatesByRoot.get(normalizedRoot) ?? [];
|
|
457
|
+
existing.push({ moduleName, absolutePath });
|
|
458
|
+
candidatesByRoot.set(normalizedRoot, existing);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// Ignore registry paths that are not present in this checkout.
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const candidates of candidatesByRoot.values()) {
|
|
466
|
+
if (options?.canonicalizeOverlappingPaths === true) {
|
|
467
|
+
const canonical = architectureContract
|
|
468
|
+
? await selectCanonicalModuleRootCandidate(workspaceRoot, architectureContract, candidates, contractCache)
|
|
469
|
+
: candidates[0];
|
|
470
|
+
if (canonical) {
|
|
471
|
+
moduleRoots.set(canonical.moduleName, canonical.absolutePath);
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
for (const candidate of candidates) {
|
|
476
|
+
moduleRoots.set(candidate.moduleName, candidate.absolutePath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (registryEntries.length > 0) {
|
|
480
|
+
return new Map([...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
481
|
+
}
|
|
482
|
+
const componentModuleRoots = getComponentModuleRoots(workspaceRoot, architectureContract);
|
|
483
|
+
if (componentModuleRoots.size > 0) {
|
|
484
|
+
for (const [moduleName, moduleRoot] of componentModuleRoots.entries()) {
|
|
485
|
+
try {
|
|
486
|
+
const stats = await node_fs_1.promises.stat(moduleRoot);
|
|
487
|
+
if (stats.isDirectory()) {
|
|
488
|
+
moduleRoots.set(moduleName, moduleRoot);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Ignore component paths that are not present in this checkout.
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (moduleRoots.size > 0) {
|
|
496
|
+
return new Map([...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const [moduleName, moduleRoot] of discoveredModuleRoots.entries()) {
|
|
500
|
+
if (!moduleRoots.has(moduleName)) {
|
|
501
|
+
moduleRoots.set(moduleName, moduleRoot);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return new Map([...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
505
|
+
}
|
|
506
|
+
async function selectCanonicalModuleRootCandidate(workspaceRoot, architectureContract, candidates, contractCache) {
|
|
507
|
+
const scoredCandidates = await Promise.all(candidates.map(async (candidate) => {
|
|
508
|
+
const contract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, candidate.moduleName, contractCache);
|
|
509
|
+
const contractDetailScore = contract.status === 'ok' && contract.contract
|
|
510
|
+
? contract.contract.dependsOn.length * 8 +
|
|
511
|
+
contract.contract.ownedTables.length * 3 +
|
|
512
|
+
contract.contract.exposedApis.length * 3 +
|
|
513
|
+
contract.contract.publicEntrypoints.length
|
|
514
|
+
: 0;
|
|
515
|
+
const shallowNameScore = candidate.moduleName.includes('/') ? 0 : 2;
|
|
516
|
+
return {
|
|
517
|
+
candidate,
|
|
518
|
+
score: contractDetailScore + shallowNameScore,
|
|
519
|
+
};
|
|
520
|
+
}));
|
|
521
|
+
return scoredCandidates
|
|
522
|
+
.sort((left, right) => {
|
|
523
|
+
const scoreCompare = right.score - left.score;
|
|
524
|
+
if (scoreCompare !== 0) {
|
|
525
|
+
return scoreCompare;
|
|
526
|
+
}
|
|
527
|
+
const lengthCompare = left.candidate.moduleName.length - right.candidate.moduleName.length;
|
|
528
|
+
if (lengthCompare !== 0) {
|
|
529
|
+
return lengthCompare;
|
|
530
|
+
}
|
|
531
|
+
return left.candidate.moduleName.localeCompare(right.candidate.moduleName);
|
|
532
|
+
})[0]?.candidate;
|
|
533
|
+
}
|
|
534
|
+
async function buildExplicitStandaloneModulePathMap(workspaceRoot, architectureContract) {
|
|
535
|
+
const moduleRegistry = architectureContract?.modules &&
|
|
536
|
+
typeof architectureContract.modules === 'object' &&
|
|
537
|
+
!Array.isArray(architectureContract.modules)
|
|
538
|
+
? architectureContract.modules
|
|
539
|
+
: undefined;
|
|
540
|
+
if (!moduleRegistry || Object.keys(moduleRegistry).length === 0) {
|
|
541
|
+
return new Map();
|
|
542
|
+
}
|
|
543
|
+
const entries = Object.entries(moduleRegistry)
|
|
544
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
545
|
+
const map = new Map();
|
|
546
|
+
for (const [moduleName, moduleReference] of entries) {
|
|
547
|
+
const configuredPath = moduleReference.path;
|
|
548
|
+
if (!configuredPath || typeof configuredPath !== 'string') {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const normalizedConfiguredPath = normalizePath(configuredPath);
|
|
552
|
+
if (!/\.[a-z0-9]+$/iu.test(normalizedConfiguredPath)) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const absolutePath = path.join(workspaceRoot, ...normalizedConfiguredPath.split('/'));
|
|
556
|
+
try {
|
|
557
|
+
const stats = await node_fs_1.promises.stat(absolutePath);
|
|
558
|
+
if (stats.isFile()) {
|
|
559
|
+
map.set(normalizedConfiguredPath, moduleName);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Ignore missing standalone paths.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return map;
|
|
567
|
+
}
|
|
568
|
+
function isTestSourcePath(relativePath) {
|
|
569
|
+
return (0, sourceScopeClassification_1.classifyValidationSourceScope)(relativePath).isTestLike;
|
|
570
|
+
}
|
|
571
|
+
async function collectSourceFiles(directoryPath, adapter, scanRootPath = directoryPath, excludedDirectoryRoots = []) {
|
|
572
|
+
const entries = await node_fs_1.promises.readdir(directoryPath, { withFileTypes: true });
|
|
573
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
574
|
+
const files = [];
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
const fullPath = path.join(directoryPath, entry.name);
|
|
577
|
+
if (entry.isDirectory()) {
|
|
578
|
+
if (excludedDirectoryRoots.some((excludedRoot) => isPathInsideOrEqual(fullPath, excludedRoot))) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const normalizedRelativeDirectory = normalizePath(path.relative(scanRootPath, fullPath));
|
|
582
|
+
if (adapter.ignoreDirectoryNames.includes(entry.name) ||
|
|
583
|
+
adapter.ignoreDirectoryNames.includes(normalizedRelativeDirectory)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
files.push(...(await collectSourceFiles(fullPath, adapter, scanRootPath, excludedDirectoryRoots)));
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (entry.isFile() &&
|
|
590
|
+
adapter.dependencyParsingFileExtensions.includes(path.extname(entry.name).toLowerCase()) &&
|
|
591
|
+
!entry.name.endsWith('.d.ts')) {
|
|
592
|
+
files.push(fullPath);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return files;
|
|
596
|
+
}
|
|
597
|
+
function extractImportSpecifiers(filePath, sourceText) {
|
|
598
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
599
|
+
const imports = [];
|
|
600
|
+
const visit = (node) => {
|
|
601
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
602
|
+
node.moduleSpecifier &&
|
|
603
|
+
ts.isStringLiteral(node.moduleSpecifier)) {
|
|
604
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.moduleSpecifier.getStart(sourceFile)).line + 1;
|
|
605
|
+
imports.push({
|
|
606
|
+
specifier: node.moduleSpecifier.text,
|
|
607
|
+
line,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (ts.isCallExpression(node) &&
|
|
611
|
+
node.arguments.length >= 1 &&
|
|
612
|
+
ts.isStringLiteral(node.arguments[0]) &&
|
|
613
|
+
((ts.isIdentifier(node.expression) && node.expression.text === 'require') ||
|
|
614
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword)) {
|
|
615
|
+
const firstArg = node.arguments[0];
|
|
616
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
617
|
+
const line = sourceFile.getLineAndCharacterOfPosition(firstArg.getStart(sourceFile)).line + 1;
|
|
618
|
+
imports.push({
|
|
619
|
+
specifier: firstArg.text,
|
|
620
|
+
line,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
ts.forEachChild(node, visit);
|
|
625
|
+
};
|
|
626
|
+
visit(sourceFile);
|
|
627
|
+
return imports.sort((left, right) => {
|
|
628
|
+
const lineCompare = left.line - right.line;
|
|
629
|
+
if (lineCompare !== 0) {
|
|
630
|
+
return lineCompare;
|
|
631
|
+
}
|
|
632
|
+
return left.specifier.localeCompare(right.specifier);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function extractJavaImportSpecifiers(sourceText) {
|
|
636
|
+
const imports = [];
|
|
637
|
+
const lines = sourceText.split(/\r?\n/u);
|
|
638
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
639
|
+
const line = lines[index];
|
|
640
|
+
const match = /^\s*import\s+(?:static\s+)?([a-zA-Z_][\w.]*\*?)\s*;\s*$/u.exec(line);
|
|
641
|
+
if (!match?.[1]) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
imports.push({
|
|
645
|
+
specifier: match[1],
|
|
646
|
+
line: index + 1,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return imports.sort((left, right) => {
|
|
650
|
+
const lineCompare = left.line - right.line;
|
|
651
|
+
if (lineCompare !== 0) {
|
|
652
|
+
return lineCompare;
|
|
653
|
+
}
|
|
654
|
+
return left.specifier.localeCompare(right.specifier);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function extractCSharpImportSpecifiers(sourceText) {
|
|
658
|
+
const imports = [];
|
|
659
|
+
const lines = sourceText.split(/\r?\n/u);
|
|
660
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
661
|
+
const line = lines[index]?.replace(/\/\/.*$/u, '').trim() ?? '';
|
|
662
|
+
if (line.length === 0) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const usingMatch = /^(?:global\s+)?using\s+([A-Za-z_][\w.]*)\s*;\s*$/u.exec(line);
|
|
666
|
+
if (usingMatch?.[1]) {
|
|
667
|
+
imports.push({
|
|
668
|
+
specifier: usingMatch[1],
|
|
669
|
+
line: index + 1,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return imports.sort((left, right) => {
|
|
674
|
+
const lineCompare = left.line - right.line;
|
|
675
|
+
if (lineCompare !== 0) {
|
|
676
|
+
return lineCompare;
|
|
677
|
+
}
|
|
678
|
+
return left.specifier.localeCompare(right.specifier);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function extractPhpImportSpecifiers(sourceText) {
|
|
682
|
+
const imports = [];
|
|
683
|
+
const lines = sourceText.split(/\r?\n/u);
|
|
684
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
685
|
+
const line = lines[index].replace(/\/\/.*$/u, '').replace(/#.*$/u, '').trim();
|
|
686
|
+
if (line.length === 0) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const useMatch = /^\s*use\s+(.+?)\s*;\s*$/iu.exec(line);
|
|
690
|
+
if (!useMatch?.[1]) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
let clause = useMatch[1].trim();
|
|
694
|
+
clause = clause.replace(/^(function|const)\s+/iu, '').trim();
|
|
695
|
+
if (clause.length === 0) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const groupedMatch = /^([a-zA-Z_\\][\w\\]*)\\\{(.+)\}$/u.exec(clause);
|
|
699
|
+
if (groupedMatch?.[1] && groupedMatch?.[2]) {
|
|
700
|
+
const baseNamespace = groupedMatch[1].replace(/^\\+/u, '');
|
|
701
|
+
for (const groupedEntry of groupedMatch[2].split(',')) {
|
|
702
|
+
const normalizedGroupedEntry = groupedEntry.trim();
|
|
703
|
+
if (normalizedGroupedEntry.length === 0) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const withoutAlias = normalizedGroupedEntry.split(/\s+as\s+/iu)[0]?.trim();
|
|
707
|
+
if (!withoutAlias) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
imports.push({
|
|
711
|
+
specifier: `${baseNamespace}\\${withoutAlias}`,
|
|
712
|
+
line: index + 1,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
for (const entry of clause.split(',')) {
|
|
718
|
+
const normalizedEntry = entry.trim();
|
|
719
|
+
if (normalizedEntry.length === 0) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const withoutAlias = normalizedEntry.split(/\s+as\s+/iu)[0]?.trim();
|
|
723
|
+
if (!withoutAlias) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
imports.push({
|
|
727
|
+
specifier: withoutAlias.replace(/^\\+/u, ''),
|
|
728
|
+
line: index + 1,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return imports.sort((left, right) => {
|
|
733
|
+
const lineCompare = left.line - right.line;
|
|
734
|
+
if (lineCompare !== 0) {
|
|
735
|
+
return lineCompare;
|
|
736
|
+
}
|
|
737
|
+
return left.specifier.localeCompare(right.specifier);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function extractGoImportSpecifiers(sourceText) {
|
|
741
|
+
const imports = [];
|
|
742
|
+
const lines = sourceText.split(/\r?\n/u);
|
|
743
|
+
let inImportBlock = false;
|
|
744
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
745
|
+
const line = lines[index].replace(/\/\/.*$/u, '').trim();
|
|
746
|
+
if (line.length === 0) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (!inImportBlock) {
|
|
750
|
+
const singleImportMatch = /^\s*import\s+(?:[._a-zA-Z][\w]*\s+)?["`]([^"`]+)["`]\s*$/u.exec(line);
|
|
751
|
+
if (singleImportMatch?.[1]) {
|
|
752
|
+
imports.push({
|
|
753
|
+
specifier: singleImportMatch[1],
|
|
754
|
+
line: index + 1,
|
|
755
|
+
});
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (/^\s*import\s*\(\s*$/u.test(line)) {
|
|
759
|
+
inImportBlock = true;
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (/^\s*\)\s*$/u.test(line)) {
|
|
764
|
+
inImportBlock = false;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const groupedImportMatch = /^(?:[._a-zA-Z][\w]*\s+)?["`]([^"`]+)["`]\s*$/u.exec(line);
|
|
768
|
+
if (!groupedImportMatch?.[1]) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
imports.push({
|
|
772
|
+
specifier: groupedImportMatch[1],
|
|
773
|
+
line: index + 1,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return imports.sort((left, right) => {
|
|
777
|
+
const lineCompare = left.line - right.line;
|
|
778
|
+
if (lineCompare !== 0) {
|
|
779
|
+
return lineCompare;
|
|
780
|
+
}
|
|
781
|
+
return left.specifier.localeCompare(right.specifier);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function extractJavaPackageName(sourceText) {
|
|
785
|
+
const match = /^\s*package\s+([a-zA-Z_][\w.]*)\s*;/mu.exec(sourceText);
|
|
786
|
+
return match?.[1];
|
|
787
|
+
}
|
|
788
|
+
async function buildJavaImportIndex(workspaceRoot, moduleRoots, adapter) {
|
|
789
|
+
const classes = new Map();
|
|
790
|
+
const packageCandidates = new Map();
|
|
791
|
+
for (const [moduleId, moduleRoot] of [...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
792
|
+
const childModuleRoots = [...moduleRoots.entries()]
|
|
793
|
+
.filter(([childModuleName, childModuleRoot]) => childModuleName !== moduleId && isPathInsideOrEqual(childModuleRoot, moduleRoot))
|
|
794
|
+
.map(([, childModuleRoot]) => childModuleRoot);
|
|
795
|
+
const sourceFiles = await collectSourceFiles(moduleRoot, adapter, moduleRoot, childModuleRoots);
|
|
796
|
+
for (const sourceFilePath of sourceFiles) {
|
|
797
|
+
const relativePath = normalizePath(path.relative(workspaceRoot, sourceFilePath));
|
|
798
|
+
if (isTestSourcePath(relativePath)) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const sourceText = await readTextFileIfExists(sourceFilePath);
|
|
802
|
+
if (!sourceText) {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const packageName = extractJavaPackageName(sourceText);
|
|
806
|
+
if (!packageName) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const className = path.basename(sourceFilePath, path.extname(sourceFilePath));
|
|
810
|
+
const entry = { moduleId, resolvedPath: sourceFilePath };
|
|
811
|
+
classes.set(`${packageName}.${className}`, entry);
|
|
812
|
+
const existing = packageCandidates.get(packageName) ?? [];
|
|
813
|
+
existing.push(entry);
|
|
814
|
+
packageCandidates.set(packageName, existing);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const packages = new Map();
|
|
818
|
+
for (const [packageName, entries] of packageCandidates) {
|
|
819
|
+
const moduleIds = new Set(entries.map((entry) => entry.moduleId));
|
|
820
|
+
packages.set(packageName, moduleIds.size === 1 ? entries[0] : 'ambiguous');
|
|
821
|
+
}
|
|
822
|
+
return { classes, packages };
|
|
823
|
+
}
|
|
824
|
+
function resolveJavaImportFromIndex(importSpecifier, index, workspaceRoot) {
|
|
825
|
+
if (!index) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
if (importSpecifier.endsWith('.*')) {
|
|
829
|
+
const packageName = importSpecifier.slice(0, -2);
|
|
830
|
+
const entry = index.packages.get(packageName);
|
|
831
|
+
if (!entry || entry === 'ambiguous') {
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
targetModule: entry.moduleId,
|
|
836
|
+
resolvedRelativePath: normalizePath(path.relative(workspaceRoot, entry.resolvedPath)),
|
|
837
|
+
isPublicImport: false,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const exactEntry = index.classes.get(importSpecifier);
|
|
841
|
+
if (exactEntry) {
|
|
842
|
+
return {
|
|
843
|
+
targetModule: exactEntry.moduleId,
|
|
844
|
+
resolvedRelativePath: normalizePath(path.relative(workspaceRoot, exactEntry.resolvedPath)),
|
|
845
|
+
isPublicImport: false,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
const ownerCandidate = [...index.classes.entries()]
|
|
849
|
+
.filter(([classImport]) => importSpecifier.startsWith(`${classImport}.`))
|
|
850
|
+
.sort((left, right) => right[0].length - left[0].length || left[0].localeCompare(right[0]))[0];
|
|
851
|
+
if (!ownerCandidate) {
|
|
852
|
+
return undefined;
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
targetModule: ownerCandidate[1].moduleId,
|
|
856
|
+
resolvedRelativePath: normalizePath(path.relative(workspaceRoot, ownerCandidate[1].resolvedPath)),
|
|
857
|
+
isPublicImport: false,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function extractPythonImportSpecifiers(sourceText) {
|
|
861
|
+
const imports = [];
|
|
862
|
+
const lines = sourceText.split(/\r?\n/u);
|
|
863
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
864
|
+
const line = lines[index].replace(/#.*$/u, '').trim();
|
|
865
|
+
if (line.length === 0) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const importMatch = /^import\s+(.+)$/u.exec(line);
|
|
869
|
+
if (importMatch?.[1]) {
|
|
870
|
+
for (const clause of importMatch[1].split(',')) {
|
|
871
|
+
const normalized = clause.trim();
|
|
872
|
+
if (normalized.length === 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const withoutAlias = normalized.split(/\s+as\s+/u)[0]?.trim();
|
|
876
|
+
if (!withoutAlias || withoutAlias.startsWith('.')) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
imports.push({
|
|
880
|
+
specifier: withoutAlias,
|
|
881
|
+
line: index + 1,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const fromMatch = /^from\s+([a-zA-Z_][\w.]*)\s+import\s+(.+)$/u.exec(line);
|
|
887
|
+
if (!fromMatch?.[1] || !fromMatch?.[2]) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const importBase = fromMatch[1].trim();
|
|
891
|
+
if (importBase.startsWith('.')) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const importedClauses = fromMatch[2].split(',');
|
|
895
|
+
if (importedClauses.length === 0) {
|
|
896
|
+
imports.push({
|
|
897
|
+
specifier: importBase,
|
|
898
|
+
line: index + 1,
|
|
899
|
+
});
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
for (const importedClause of importedClauses) {
|
|
903
|
+
const normalized = importedClause.trim();
|
|
904
|
+
if (normalized.length === 0) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const withoutAlias = normalized.split(/\s+as\s+/u)[0]?.trim();
|
|
908
|
+
if (!withoutAlias) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (withoutAlias === '*') {
|
|
912
|
+
imports.push({
|
|
913
|
+
specifier: importBase,
|
|
914
|
+
line: index + 1,
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
imports.push({
|
|
919
|
+
specifier: `${importBase}.${withoutAlias}`,
|
|
920
|
+
line: index + 1,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return imports.sort((left, right) => {
|
|
925
|
+
const lineCompare = left.line - right.line;
|
|
926
|
+
if (lineCompare !== 0) {
|
|
927
|
+
return lineCompare;
|
|
928
|
+
}
|
|
929
|
+
return left.specifier.localeCompare(right.specifier);
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function extractImportSpecifiersForAdapter(adapter, filePath, sourceText) {
|
|
933
|
+
if (usesJavaImportParser(adapter)) {
|
|
934
|
+
return extractJavaImportSpecifiers(sourceText);
|
|
935
|
+
}
|
|
936
|
+
if (usesCSharpImportParser(adapter)) {
|
|
937
|
+
return extractCSharpImportSpecifiers(sourceText);
|
|
938
|
+
}
|
|
939
|
+
if (usesPhpImportParser(adapter)) {
|
|
940
|
+
return extractPhpImportSpecifiers(sourceText);
|
|
941
|
+
}
|
|
942
|
+
if (usesGoImportParser(adapter)) {
|
|
943
|
+
return extractGoImportSpecifiers(sourceText);
|
|
944
|
+
}
|
|
945
|
+
if (usesPythonImportParser(adapter)) {
|
|
946
|
+
return extractPythonImportSpecifiers(sourceText);
|
|
947
|
+
}
|
|
948
|
+
return extractImportSpecifiers(filePath, sourceText);
|
|
949
|
+
}
|
|
950
|
+
function resolveJavaModuleImport(modulesRootRelativePath, moduleRoots, importSpecifier, adapter) {
|
|
951
|
+
const segments = importSpecifier.split('.').filter((segment) => segment.length > 0);
|
|
952
|
+
if (segments.length < 2) {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
const targetModule = segments.find((segment) => moduleRoots.has(segment));
|
|
956
|
+
if (!targetModule) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
const lastSegment = segments[segments.length - 1];
|
|
960
|
+
const defaultEntrypointFileName = adapter.scaffoldingDefaults?.moduleIndexFileName ?? 'Application.java';
|
|
961
|
+
const resolvedFileName = lastSegment === '*'
|
|
962
|
+
? defaultEntrypointFileName
|
|
963
|
+
: lastSegment.endsWith('.java')
|
|
964
|
+
? lastSegment
|
|
965
|
+
: `${lastSegment}.java`;
|
|
966
|
+
const resolvedRelativePath = normalizePath(`${modulesRootRelativePath}/${targetModule}/${resolvedFileName}`);
|
|
967
|
+
return {
|
|
968
|
+
targetModule,
|
|
969
|
+
resolvedRelativePath,
|
|
970
|
+
isPublicImport: resolvedFileName === defaultEntrypointFileName || lastSegment === '*',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function findCaseInsensitiveModuleId(moduleRoots, segment) {
|
|
974
|
+
return [...moduleRoots.keys()].find((moduleName) => moduleName.toLowerCase() === segment.toLowerCase());
|
|
975
|
+
}
|
|
976
|
+
function resolveCSharpModuleImportBasePaths(moduleRoots, importSpecifier, adapter) {
|
|
977
|
+
const segments = importSpecifier.split('.').filter((segment) => segment.length > 0);
|
|
978
|
+
if (segments.length === 0) {
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
const targetModuleSegment = segments.find((segment) => findCaseInsensitiveModuleId(moduleRoots, segment));
|
|
982
|
+
if (!targetModuleSegment) {
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
const targetModule = findCaseInsensitiveModuleId(moduleRoots, targetModuleSegment);
|
|
986
|
+
if (!targetModule) {
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
const targetModuleRoot = moduleRoots.get(targetModule);
|
|
990
|
+
if (!targetModuleRoot) {
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
targetModule,
|
|
995
|
+
candidateBasePaths: [
|
|
996
|
+
path.join(targetModuleRoot, ...segments
|
|
997
|
+
.slice(segments.indexOf(targetModuleSegment) + 1)
|
|
998
|
+
.filter((segment) => segment.length > 0)),
|
|
999
|
+
targetModuleRoot,
|
|
1000
|
+
],
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function resolvePythonModuleImportBasePaths(moduleRoots, importSpecifier, adapter) {
|
|
1004
|
+
const segments = importSpecifier.split('.').filter((segment) => segment.length > 0);
|
|
1005
|
+
if (segments.length === 0) {
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
const targetModuleSegment = segments.find((segment) => findCaseInsensitiveModuleId(moduleRoots, segment));
|
|
1009
|
+
if (!targetModuleSegment) {
|
|
1010
|
+
return undefined;
|
|
1011
|
+
}
|
|
1012
|
+
const targetModule = findCaseInsensitiveModuleId(moduleRoots, targetModuleSegment);
|
|
1013
|
+
if (!targetModule) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
const targetModuleRoot = moduleRoots.get(targetModule);
|
|
1017
|
+
if (!targetModuleRoot) {
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
const remainder = segments.slice(segments.indexOf(targetModuleSegment) + 1).filter((segment) => segment.length > 0);
|
|
1021
|
+
const candidateBasePaths = remainder.length === 0
|
|
1022
|
+
? [targetModuleRoot]
|
|
1023
|
+
: [
|
|
1024
|
+
path.join(targetModuleRoot, ...remainder),
|
|
1025
|
+
...(remainder.length > 1 ? [path.join(targetModuleRoot, ...remainder.slice(0, -1))] : []),
|
|
1026
|
+
targetModuleRoot,
|
|
1027
|
+
];
|
|
1028
|
+
return {
|
|
1029
|
+
targetModule,
|
|
1030
|
+
candidateBasePaths,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function resolvePhpModuleImportBasePaths(moduleRoots, importSpecifier, adapter) {
|
|
1034
|
+
const segments = importSpecifier
|
|
1035
|
+
.split('\\')
|
|
1036
|
+
.map((segment) => segment.trim())
|
|
1037
|
+
.filter((segment) => segment.length > 0);
|
|
1038
|
+
if (segments.length === 0) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
if (adapter.id === 'laravel' && segments[0] !== 'App') {
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
const targetModuleSegment = segments.find((segment) => findCaseInsensitiveModuleId(moduleRoots, segment));
|
|
1045
|
+
if (!targetModuleSegment) {
|
|
1046
|
+
return undefined;
|
|
1047
|
+
}
|
|
1048
|
+
const targetModule = findCaseInsensitiveModuleId(moduleRoots, targetModuleSegment);
|
|
1049
|
+
if (!targetModule) {
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
const targetModuleRoot = moduleRoots.get(targetModule);
|
|
1053
|
+
if (!targetModuleRoot) {
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
const remainder = segments.slice(segments.indexOf(targetModuleSegment) + 1).filter((segment) => segment.length > 0);
|
|
1057
|
+
return {
|
|
1058
|
+
targetModule,
|
|
1059
|
+
candidateBasePaths: remainder.length === 0
|
|
1060
|
+
? [targetModuleRoot]
|
|
1061
|
+
: [path.join(targetModuleRoot, ...remainder), targetModuleRoot],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function resolveGoModuleImportBasePaths(moduleRoots, importSpecifier, adapter) {
|
|
1065
|
+
const segments = importSpecifier
|
|
1066
|
+
.split('/')
|
|
1067
|
+
.map((segment) => segment.trim())
|
|
1068
|
+
.filter((segment) => segment.length > 0);
|
|
1069
|
+
if (segments.length === 0) {
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
const targetModuleSegment = segments.find((segment) => findCaseInsensitiveModuleId(moduleRoots, segment));
|
|
1073
|
+
if (!targetModuleSegment) {
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
const targetModule = findCaseInsensitiveModuleId(moduleRoots, targetModuleSegment);
|
|
1077
|
+
if (!targetModule) {
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1080
|
+
const targetModuleRoot = moduleRoots.get(targetModule);
|
|
1081
|
+
if (!targetModuleRoot) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
const remainder = segments.slice(segments.indexOf(targetModuleSegment) + 1).filter((segment) => segment.length > 0);
|
|
1085
|
+
return {
|
|
1086
|
+
targetModule,
|
|
1087
|
+
candidateBasePaths: remainder.length === 0
|
|
1088
|
+
? [targetModuleRoot]
|
|
1089
|
+
: [path.join(targetModuleRoot, ...remainder), targetModuleRoot],
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async function resolveImportPath(basePath, adapter) {
|
|
1093
|
+
const relevantExtensions = [...new Set(adapter.dependencyParsingFileExtensions)];
|
|
1094
|
+
const moduleIndexFileName = adapter.scaffoldingDefaults?.moduleIndexFileName;
|
|
1095
|
+
const candidates = [
|
|
1096
|
+
basePath,
|
|
1097
|
+
...relevantExtensions.map((extension) => `${basePath}${extension}`),
|
|
1098
|
+
...relevantExtensions.map((extension) => path.join(basePath, `index${extension}`)),
|
|
1099
|
+
...(moduleIndexFileName ? [path.join(basePath, moduleIndexFileName)] : []),
|
|
1100
|
+
];
|
|
1101
|
+
for (const candidate of candidates) {
|
|
1102
|
+
try {
|
|
1103
|
+
const stats = await node_fs_1.promises.stat(candidate);
|
|
1104
|
+
if (stats.isFile()) {
|
|
1105
|
+
return candidate;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
// Ignore resolution misses and continue to the next candidate.
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
function buildAdapterResolutionKey(adapter) {
|
|
1115
|
+
return [
|
|
1116
|
+
adapter.id,
|
|
1117
|
+
[...new Set(adapter.dependencyParsingFileExtensions)].join(','),
|
|
1118
|
+
adapter.scaffoldingDefaults?.moduleIndexFileName ?? '',
|
|
1119
|
+
].join('|');
|
|
1120
|
+
}
|
|
1121
|
+
function buildWorkspacePackageAliasKey(workspacePackageAliases) {
|
|
1122
|
+
return workspacePackageAliases
|
|
1123
|
+
.map((alias) => [
|
|
1124
|
+
alias.packageName,
|
|
1125
|
+
normalizeAbsolutePath(alias.moduleRoot),
|
|
1126
|
+
alias.targetRoot ? normalizeAbsolutePath(alias.targetRoot) : '',
|
|
1127
|
+
alias.targetFile ? normalizeAbsolutePath(alias.targetFile) : '',
|
|
1128
|
+
alias.publicExportSubpaths?.join(',') ?? '',
|
|
1129
|
+
alias.moduleId,
|
|
1130
|
+
].join(':'))
|
|
1131
|
+
.sort((left, right) => left.localeCompare(right))
|
|
1132
|
+
.join('|');
|
|
1133
|
+
}
|
|
1134
|
+
function createDependencyGraphResolutionContext() {
|
|
1135
|
+
const resolveImportPathCache = new Map();
|
|
1136
|
+
const workspacePackageCache = new Map();
|
|
1137
|
+
const fileStatCache = new Map();
|
|
1138
|
+
const stats = {
|
|
1139
|
+
resolveImportPathCalls: 0,
|
|
1140
|
+
resolveImportPathCacheHits: 0,
|
|
1141
|
+
resolveImportPathCacheMisses: 0,
|
|
1142
|
+
resolveImportPathCacheEntries: 0,
|
|
1143
|
+
workspacePackageResolutionCalls: 0,
|
|
1144
|
+
workspacePackageResolutionCacheHits: 0,
|
|
1145
|
+
workspacePackageResolutionCacheMisses: 0,
|
|
1146
|
+
workspacePackageResolutionCacheEntries: 0,
|
|
1147
|
+
underlyingStatProbes: 0,
|
|
1148
|
+
statCacheHits: 0,
|
|
1149
|
+
statMisses: 0,
|
|
1150
|
+
negativeResolutionCacheHits: 0,
|
|
1151
|
+
resolveImportPathTimeMs: 0,
|
|
1152
|
+
workspacePackageResolutionTimeMs: 0,
|
|
1153
|
+
statProbeTimeMs: 0,
|
|
1154
|
+
};
|
|
1155
|
+
const isFile = (candidatePath) => {
|
|
1156
|
+
const key = normalizeAbsolutePath(candidatePath);
|
|
1157
|
+
const cached = fileStatCache.get(key);
|
|
1158
|
+
if (cached) {
|
|
1159
|
+
stats.statCacheHits += 1;
|
|
1160
|
+
return cached;
|
|
1161
|
+
}
|
|
1162
|
+
stats.underlyingStatProbes += 1;
|
|
1163
|
+
const started = Date.now();
|
|
1164
|
+
const statPromise = node_fs_1.promises.stat(candidatePath)
|
|
1165
|
+
.then((candidateStats) => candidateStats.isFile())
|
|
1166
|
+
.catch(() => {
|
|
1167
|
+
stats.statMisses += 1;
|
|
1168
|
+
return false;
|
|
1169
|
+
})
|
|
1170
|
+
.finally(() => {
|
|
1171
|
+
stats.statProbeTimeMs += Date.now() - started;
|
|
1172
|
+
});
|
|
1173
|
+
fileStatCache.set(key, statPromise);
|
|
1174
|
+
return statPromise;
|
|
1175
|
+
};
|
|
1176
|
+
const resolvePath = async (basePath, adapter) => {
|
|
1177
|
+
const relevantExtensions = [...new Set(adapter.dependencyParsingFileExtensions)];
|
|
1178
|
+
const moduleIndexFileName = adapter.scaffoldingDefaults?.moduleIndexFileName;
|
|
1179
|
+
const candidates = [
|
|
1180
|
+
basePath,
|
|
1181
|
+
...relevantExtensions.map((extension) => `${basePath}${extension}`),
|
|
1182
|
+
...relevantExtensions.map((extension) => path.join(basePath, `index${extension}`)),
|
|
1183
|
+
...(moduleIndexFileName ? [path.join(basePath, moduleIndexFileName)] : []),
|
|
1184
|
+
];
|
|
1185
|
+
for (const candidate of candidates) {
|
|
1186
|
+
if (await isFile(candidate)) {
|
|
1187
|
+
return candidate;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return undefined;
|
|
1191
|
+
};
|
|
1192
|
+
const context = {
|
|
1193
|
+
resolveImportPath(basePath, adapter) {
|
|
1194
|
+
stats.resolveImportPathCalls += 1;
|
|
1195
|
+
const started = Date.now();
|
|
1196
|
+
const key = `${normalizeAbsolutePath(basePath)}|${buildAdapterResolutionKey(adapter)}`;
|
|
1197
|
+
const cached = resolveImportPathCache.get(key);
|
|
1198
|
+
if (cached) {
|
|
1199
|
+
stats.resolveImportPathCacheHits += 1;
|
|
1200
|
+
return cached.then((resolvedPath) => {
|
|
1201
|
+
if (!resolvedPath) {
|
|
1202
|
+
stats.negativeResolutionCacheHits += 1;
|
|
1203
|
+
}
|
|
1204
|
+
stats.resolveImportPathTimeMs += Date.now() - started;
|
|
1205
|
+
return resolvedPath;
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
stats.resolveImportPathCacheMisses += 1;
|
|
1209
|
+
const resolutionPromise = resolvePath(basePath, adapter);
|
|
1210
|
+
resolveImportPathCache.set(key, resolutionPromise);
|
|
1211
|
+
stats.resolveImportPathCacheEntries = resolveImportPathCache.size;
|
|
1212
|
+
return resolutionPromise.finally(() => {
|
|
1213
|
+
stats.resolveImportPathTimeMs += Date.now() - started;
|
|
1214
|
+
});
|
|
1215
|
+
},
|
|
1216
|
+
resolveWorkspacePackageImportTarget(importSpecifier, workspacePackageAliases, adapter) {
|
|
1217
|
+
stats.workspacePackageResolutionCalls += 1;
|
|
1218
|
+
const started = Date.now();
|
|
1219
|
+
const key = [
|
|
1220
|
+
importSpecifier,
|
|
1221
|
+
buildAdapterResolutionKey(adapter),
|
|
1222
|
+
buildWorkspacePackageAliasKey(workspacePackageAliases),
|
|
1223
|
+
].join('|');
|
|
1224
|
+
const cached = workspacePackageCache.get(key);
|
|
1225
|
+
if (cached) {
|
|
1226
|
+
stats.workspacePackageResolutionCacheHits += 1;
|
|
1227
|
+
return cached.finally(() => {
|
|
1228
|
+
stats.workspacePackageResolutionTimeMs += Date.now() - started;
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
stats.workspacePackageResolutionCacheMisses += 1;
|
|
1232
|
+
const resolutionPromise = resolveWorkspacePackageImportTarget(importSpecifier, workspacePackageAliases, adapter, context);
|
|
1233
|
+
workspacePackageCache.set(key, resolutionPromise);
|
|
1234
|
+
stats.workspacePackageResolutionCacheEntries = workspacePackageCache.size;
|
|
1235
|
+
return resolutionPromise.finally(() => {
|
|
1236
|
+
stats.workspacePackageResolutionTimeMs += Date.now() - started;
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
getStats() {
|
|
1240
|
+
return { ...stats };
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
return context;
|
|
1244
|
+
}
|
|
1245
|
+
function resolvePackageAliasRemainder(packageName, importSpecifier) {
|
|
1246
|
+
if (importSpecifier === packageName) {
|
|
1247
|
+
return '';
|
|
1248
|
+
}
|
|
1249
|
+
if (importSpecifier.startsWith(`${packageName}/`)) {
|
|
1250
|
+
return importSpecifier.slice(packageName.length + 1);
|
|
1251
|
+
}
|
|
1252
|
+
return undefined;
|
|
1253
|
+
}
|
|
1254
|
+
function packageExportPatternMatches(pattern, subpath) {
|
|
1255
|
+
if (!pattern.includes('*')) {
|
|
1256
|
+
return pattern === subpath;
|
|
1257
|
+
}
|
|
1258
|
+
const [prefix, suffix = ''] = pattern.split('*', 2);
|
|
1259
|
+
return subpath.startsWith(prefix) && subpath.endsWith(suffix);
|
|
1260
|
+
}
|
|
1261
|
+
function isImportSpecifierPackageExport(alias, importSpecifier) {
|
|
1262
|
+
const remainder = resolvePackageAliasRemainder(alias.packageName, importSpecifier);
|
|
1263
|
+
if (remainder === undefined) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
const exportSubpaths = alias.publicExportSubpaths ?? [];
|
|
1267
|
+
if (exportSubpaths.length === 0) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
const importSubpath = remainder.length === 0 ? '.' : `./${remainder}`;
|
|
1271
|
+
return exportSubpaths.some((exportSubpath) => packageExportPatternMatches(exportSubpath, importSubpath));
|
|
1272
|
+
}
|
|
1273
|
+
async function resolveWorkspacePackageImportTarget(importSpecifier, workspacePackageAliases, adapter, resolutionContext) {
|
|
1274
|
+
const packageAlias = workspacePackageAliases
|
|
1275
|
+
.map((alias) => ({
|
|
1276
|
+
alias,
|
|
1277
|
+
remainder: resolvePackageAliasRemainder(alias.packageName, importSpecifier),
|
|
1278
|
+
}))
|
|
1279
|
+
.sort((left, right) => {
|
|
1280
|
+
const leftCanResolveRemainder = left.remainder !== undefined &&
|
|
1281
|
+
left.remainder.length > 0 &&
|
|
1282
|
+
left.alias.targetRoot !== undefined;
|
|
1283
|
+
const rightCanResolveRemainder = right.remainder !== undefined &&
|
|
1284
|
+
right.remainder.length > 0 &&
|
|
1285
|
+
right.alias.targetRoot !== undefined;
|
|
1286
|
+
if (leftCanResolveRemainder !== rightCanResolveRemainder) {
|
|
1287
|
+
return leftCanResolveRemainder ? -1 : 1;
|
|
1288
|
+
}
|
|
1289
|
+
return 0;
|
|
1290
|
+
})
|
|
1291
|
+
.find((candidate) => candidate.remainder !== undefined);
|
|
1292
|
+
if (!packageAlias) {
|
|
1293
|
+
return undefined;
|
|
1294
|
+
}
|
|
1295
|
+
const { alias, remainder } = packageAlias;
|
|
1296
|
+
if (remainder.length === 0) {
|
|
1297
|
+
const resolvePath = resolutionContext?.resolveImportPath.bind(resolutionContext) ?? resolveImportPath;
|
|
1298
|
+
const resolvedPath = await resolvePath(alias.targetFile ?? alias.moduleRoot, adapter) ??
|
|
1299
|
+
(alias.targetRoot && alias.targetRoot !== alias.moduleRoot
|
|
1300
|
+
? await resolvePath(alias.targetRoot, adapter)
|
|
1301
|
+
: undefined);
|
|
1302
|
+
return resolvedPath
|
|
1303
|
+
? {
|
|
1304
|
+
alias,
|
|
1305
|
+
resolvedPath,
|
|
1306
|
+
isPublicPackageImport: isImportSpecifierPackageExport(alias, importSpecifier),
|
|
1307
|
+
}
|
|
1308
|
+
: undefined;
|
|
1309
|
+
}
|
|
1310
|
+
if (alias.targetRoot) {
|
|
1311
|
+
const resolvePath = resolutionContext?.resolveImportPath.bind(resolutionContext) ?? resolveImportPath;
|
|
1312
|
+
const resolvedPath = await resolvePath(path.join(alias.targetRoot, ...remainder.split('/').filter((segment) => segment.length > 0)), adapter);
|
|
1313
|
+
return resolvedPath
|
|
1314
|
+
? {
|
|
1315
|
+
alias,
|
|
1316
|
+
resolvedPath,
|
|
1317
|
+
isPublicPackageImport: isImportSpecifierPackageExport(alias, importSpecifier),
|
|
1318
|
+
}
|
|
1319
|
+
: undefined;
|
|
1320
|
+
}
|
|
1321
|
+
return undefined;
|
|
1322
|
+
}
|
|
1323
|
+
async function resolveCrossModuleImport(workspaceRoot, modulesRootRelativePath, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, sourceFilePath, importSpecifier, adapter, javaImportIndex, resolutionContext) {
|
|
1324
|
+
const candidateBasePaths = [];
|
|
1325
|
+
let packageAliasTargetModule;
|
|
1326
|
+
let packageAliasTargetModuleRoot;
|
|
1327
|
+
let isPublicPackageAliasImport = false;
|
|
1328
|
+
const normalizedModulesRoot = normalizePath(modulesRootRelativePath).replace(/^\/+/, '');
|
|
1329
|
+
if (usesJavaImportParser(adapter) && !importSpecifier.startsWith('.')) {
|
|
1330
|
+
const indexedJava = resolveJavaImportFromIndex(importSpecifier, javaImportIndex, workspaceRoot);
|
|
1331
|
+
if (indexedJava) {
|
|
1332
|
+
return indexedJava;
|
|
1333
|
+
}
|
|
1334
|
+
const resolvedJava = resolveJavaModuleImport(normalizedModulesRoot, moduleRoots, importSpecifier, adapter);
|
|
1335
|
+
if (resolvedJava) {
|
|
1336
|
+
return resolvedJava;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (usesCSharpImportParser(adapter) && !importSpecifier.startsWith('.')) {
|
|
1340
|
+
const resolvedCSharp = resolveCSharpModuleImportBasePaths(moduleRoots, importSpecifier, adapter);
|
|
1341
|
+
if (resolvedCSharp) {
|
|
1342
|
+
candidateBasePaths.push(...resolvedCSharp.candidateBasePaths);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (usesPhpImportParser(adapter)) {
|
|
1346
|
+
const resolvedPhp = resolvePhpModuleImportBasePaths(moduleRoots, importSpecifier, adapter);
|
|
1347
|
+
if (resolvedPhp) {
|
|
1348
|
+
candidateBasePaths.push(...resolvedPhp.candidateBasePaths);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (usesGoImportParser(adapter)) {
|
|
1352
|
+
const resolvedGo = resolveGoModuleImportBasePaths(moduleRoots, importSpecifier, adapter);
|
|
1353
|
+
if (resolvedGo) {
|
|
1354
|
+
candidateBasePaths.push(...resolvedGo.candidateBasePaths);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (usesPythonImportParser(adapter) && !importSpecifier.startsWith('.')) {
|
|
1358
|
+
const resolvedPython = resolvePythonModuleImportBasePaths(moduleRoots, importSpecifier, adapter);
|
|
1359
|
+
if (resolvedPython) {
|
|
1360
|
+
candidateBasePaths.push(...resolvedPython.candidateBasePaths);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (importSpecifier.startsWith('.')) {
|
|
1364
|
+
candidateBasePaths.push(path.resolve(path.dirname(sourceFilePath), importSpecifier));
|
|
1365
|
+
}
|
|
1366
|
+
else if (importSpecifier.startsWith(`${normalizedModulesRoot}/`)) {
|
|
1367
|
+
candidateBasePaths.push(path.join(workspaceRoot, importSpecifier));
|
|
1368
|
+
}
|
|
1369
|
+
else if (importSpecifier.startsWith(`/${normalizedModulesRoot}/`)) {
|
|
1370
|
+
candidateBasePaths.push(path.join(workspaceRoot, importSpecifier.slice(1)));
|
|
1371
|
+
}
|
|
1372
|
+
else if (normalizedModulesRoot.startsWith('src/') &&
|
|
1373
|
+
importSpecifier.startsWith(`${normalizedModulesRoot.slice('src/'.length)}/`)) {
|
|
1374
|
+
candidateBasePaths.push(path.join(workspaceRoot, 'src', importSpecifier));
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
const resolveWorkspacePackageTarget = resolutionContext?.resolveWorkspacePackageImportTarget.bind(resolutionContext) ??
|
|
1378
|
+
resolveWorkspacePackageImportTarget;
|
|
1379
|
+
const workspacePackageImportTarget = await resolveWorkspacePackageTarget(importSpecifier, workspacePackageAliases, adapter);
|
|
1380
|
+
if (workspacePackageImportTarget) {
|
|
1381
|
+
candidateBasePaths.push(workspacePackageImportTarget.resolvedPath);
|
|
1382
|
+
packageAliasTargetModule = workspacePackageImportTarget.alias.moduleId;
|
|
1383
|
+
packageAliasTargetModuleRoot = workspacePackageImportTarget.alias.moduleRoot;
|
|
1384
|
+
isPublicPackageAliasImport = workspacePackageImportTarget.isPublicPackageImport;
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
const moduleImportTarget = [...moduleRoots.entries()]
|
|
1388
|
+
.filter(([moduleId]) => importSpecifier === moduleId || importSpecifier.startsWith(`${moduleId}/`))
|
|
1389
|
+
.sort((left, right) => right[0].length - left[0].length || left[0].localeCompare(right[0]))[0];
|
|
1390
|
+
if (moduleImportTarget) {
|
|
1391
|
+
const [moduleId, moduleRoot] = moduleImportTarget;
|
|
1392
|
+
const remainder = importSpecifier === moduleId ? '' : importSpecifier.slice(moduleId.length + 1);
|
|
1393
|
+
candidateBasePaths.push(path.join(moduleRoot, ...remainder.split('/').filter((segment) => segment.length > 0)));
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (candidateBasePaths.length === 0) {
|
|
1398
|
+
return undefined;
|
|
1399
|
+
}
|
|
1400
|
+
let resolvedPath;
|
|
1401
|
+
const resolvePath = resolutionContext?.resolveImportPath.bind(resolutionContext) ?? resolveImportPath;
|
|
1402
|
+
for (const candidateBasePath of candidateBasePaths) {
|
|
1403
|
+
resolvedPath = await resolvePath(candidateBasePath, adapter);
|
|
1404
|
+
if (resolvedPath) {
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (!resolvedPath) {
|
|
1409
|
+
return undefined;
|
|
1410
|
+
}
|
|
1411
|
+
const targetModuleEntry = packageAliasTargetModule && packageAliasTargetModuleRoot
|
|
1412
|
+
? [packageAliasTargetModule, packageAliasTargetModuleRoot]
|
|
1413
|
+
: [...moduleRoots.entries()]
|
|
1414
|
+
.filter(([, moduleRoot]) => isPathInsideOrEqual(resolvedPath, moduleRoot))
|
|
1415
|
+
.sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]))[0];
|
|
1416
|
+
if (!targetModuleEntry) {
|
|
1417
|
+
const standaloneTargetModule = explicitStandaloneModuleByPath.get(normalizePath(path.relative(workspaceRoot, resolvedPath)));
|
|
1418
|
+
if (!standaloneTargetModule) {
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
targetModule: standaloneTargetModule,
|
|
1423
|
+
resolvedRelativePath: normalizePath(path.relative(workspaceRoot, resolvedPath)),
|
|
1424
|
+
isPublicImport: true,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
const [targetModule, targetModuleRoot] = targetModuleEntry;
|
|
1428
|
+
const targetSubPath = normalizePath(path.relative(targetModuleRoot, resolvedPath));
|
|
1429
|
+
if (targetSubPath === '' || targetSubPath.startsWith('../') || targetSubPath.startsWith('..\\')) {
|
|
1430
|
+
return undefined;
|
|
1431
|
+
}
|
|
1432
|
+
const indexEntrypointCandidates = adapter.dependencyParsingFileExtensions.map((extension) => `index${extension}`);
|
|
1433
|
+
const adapterModuleEntrypoint = adapter.scaffoldingDefaults?.moduleIndexFileName;
|
|
1434
|
+
const nestedIndexEntrypointCandidates = adapter.dependencyParsingFileExtensions.flatMap((extension) => [`src/index${extension}`, `public/index${extension}`]);
|
|
1435
|
+
const isPublicImport = isPublicPackageAliasImport ||
|
|
1436
|
+
(adapterModuleEntrypoint !== undefined && targetSubPath === adapterModuleEntrypoint) ||
|
|
1437
|
+
indexEntrypointCandidates.includes(targetSubPath) ||
|
|
1438
|
+
nestedIndexEntrypointCandidates.includes(targetSubPath) ||
|
|
1439
|
+
adapter.publicEntrypointDirectoryNames.some((dirName) => targetSubPath.startsWith(`${dirName}/`));
|
|
1440
|
+
return {
|
|
1441
|
+
targetModule,
|
|
1442
|
+
resolvedRelativePath: normalizePath(path.relative(workspaceRoot, resolvedPath)),
|
|
1443
|
+
isPublicImport,
|
|
1444
|
+
...(isPublicPackageAliasImport ? { isPackagePublicImport: true } : {}),
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async function collectCrossModuleImports(workspaceRoot, modulesRootRelativePath, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, adapter, options) {
|
|
1448
|
+
const imports = [];
|
|
1449
|
+
const includeTestFiles = options?.includeTestFiles === true;
|
|
1450
|
+
if (!supportsDependencyImportParsing(adapter)) {
|
|
1451
|
+
return imports;
|
|
1452
|
+
}
|
|
1453
|
+
const javaImportIndex = usesJavaImportParser(adapter)
|
|
1454
|
+
? await buildJavaImportIndex(workspaceRoot, moduleRoots, adapter)
|
|
1455
|
+
: undefined;
|
|
1456
|
+
for (const [moduleName, moduleRoot] of [...moduleRoots.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1457
|
+
const childModuleRoots = [...moduleRoots.entries()]
|
|
1458
|
+
.filter(([childModuleName, childModuleRoot]) => childModuleName !== moduleName && isPathInsideOrEqual(childModuleRoot, moduleRoot))
|
|
1459
|
+
.map(([, childModuleRoot]) => childModuleRoot);
|
|
1460
|
+
const sourceFiles = await collectSourceFiles(moduleRoot, adapter, moduleRoot, childModuleRoots);
|
|
1461
|
+
for (const sourceFilePath of sourceFiles) {
|
|
1462
|
+
const sourceFileRelativePath = normalizePath(path.relative(workspaceRoot, sourceFilePath));
|
|
1463
|
+
if (!includeTestFiles && isTestSourcePath(sourceFileRelativePath)) {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const sourceText = await readTextFileIfExists(sourceFilePath);
|
|
1467
|
+
if (!sourceText) {
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const importSpecifiers = extractImportSpecifiersForAdapter(adapter, sourceFilePath, sourceText);
|
|
1471
|
+
for (const importReference of importSpecifiers) {
|
|
1472
|
+
const resolved = await resolveCrossModuleImport(workspaceRoot, modulesRootRelativePath, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, sourceFilePath, importReference.specifier, adapter, javaImportIndex, options?.resolutionContext);
|
|
1473
|
+
if (!resolved || resolved.targetModule === moduleName) {
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
imports.push({
|
|
1477
|
+
sourceModule: moduleName,
|
|
1478
|
+
sourceFileRelativePath,
|
|
1479
|
+
importSpecifier: importReference.specifier,
|
|
1480
|
+
line: importReference.line,
|
|
1481
|
+
targetModule: resolved.targetModule,
|
|
1482
|
+
isPublicImport: resolved.isPublicImport,
|
|
1483
|
+
...(resolved.isPackagePublicImport ? { isPackagePublicImport: true } : {}),
|
|
1484
|
+
resolvedTargetRelativePath: resolved.resolvedRelativePath,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return imports.sort((left, right) => {
|
|
1490
|
+
const fileCompare = left.sourceFileRelativePath.localeCompare(right.sourceFileRelativePath);
|
|
1491
|
+
if (fileCompare !== 0) {
|
|
1492
|
+
return fileCompare;
|
|
1493
|
+
}
|
|
1494
|
+
const lineCompare = left.line - right.line;
|
|
1495
|
+
if (lineCompare !== 0) {
|
|
1496
|
+
return lineCompare;
|
|
1497
|
+
}
|
|
1498
|
+
const sourceCompare = left.sourceModule.localeCompare(right.sourceModule);
|
|
1499
|
+
if (sourceCompare !== 0) {
|
|
1500
|
+
return sourceCompare;
|
|
1501
|
+
}
|
|
1502
|
+
return left.targetModule.localeCompare(right.targetModule);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function canonicalizeCycle(cycle) {
|
|
1506
|
+
const uniqueNodes = cycle.slice(0, -1);
|
|
1507
|
+
const rotations = uniqueNodes.map((_, index) => {
|
|
1508
|
+
const rotated = [...uniqueNodes.slice(index), ...uniqueNodes.slice(0, index)];
|
|
1509
|
+
return `${rotated.join('->')}->${rotated[0]}`;
|
|
1510
|
+
});
|
|
1511
|
+
return rotations.sort((left, right) => left.localeCompare(right))[0];
|
|
1512
|
+
}
|
|
1513
|
+
function detectCircularDependencies(moduleNames, imports) {
|
|
1514
|
+
const adjacency = new Map();
|
|
1515
|
+
for (const moduleName of moduleNames) {
|
|
1516
|
+
adjacency.set(moduleName, []);
|
|
1517
|
+
}
|
|
1518
|
+
for (const dependencyImport of imports) {
|
|
1519
|
+
const neighbors = adjacency.get(dependencyImport.sourceModule) ?? [];
|
|
1520
|
+
if (!neighbors.includes(dependencyImport.targetModule)) {
|
|
1521
|
+
neighbors.push(dependencyImport.targetModule);
|
|
1522
|
+
neighbors.sort((left, right) => left.localeCompare(right));
|
|
1523
|
+
adjacency.set(dependencyImport.sourceModule, neighbors);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const cycles = new Set();
|
|
1527
|
+
const visited = new Set();
|
|
1528
|
+
const stack = [];
|
|
1529
|
+
const visit = (moduleName) => {
|
|
1530
|
+
visited.add(moduleName);
|
|
1531
|
+
stack.push(moduleName);
|
|
1532
|
+
for (const neighbor of adjacency.get(moduleName) ?? []) {
|
|
1533
|
+
const stackIndex = stack.indexOf(neighbor);
|
|
1534
|
+
if (stackIndex !== -1) {
|
|
1535
|
+
const cycle = [...stack.slice(stackIndex), neighbor];
|
|
1536
|
+
cycles.add(canonicalizeCycle(cycle));
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
if (!visited.has(neighbor)) {
|
|
1540
|
+
visit(neighbor);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
stack.pop();
|
|
1544
|
+
};
|
|
1545
|
+
for (const moduleName of [...moduleNames].sort((left, right) => left.localeCompare(right))) {
|
|
1546
|
+
if (!visited.has(moduleName)) {
|
|
1547
|
+
visit(moduleName);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return [...cycles].sort((left, right) => left.localeCompare(right));
|
|
1551
|
+
}
|
|
1552
|
+
function makeValidationResult(id, severity, passed, message, scope) {
|
|
1553
|
+
return {
|
|
1554
|
+
id,
|
|
1555
|
+
severity,
|
|
1556
|
+
passed,
|
|
1557
|
+
message,
|
|
1558
|
+
...(scope ?? {}),
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function toDependencyContextMetadata(context) {
|
|
1562
|
+
return {
|
|
1563
|
+
dependencyType: context.dependencyType,
|
|
1564
|
+
sourceComponentId: context.source.componentId,
|
|
1565
|
+
sourceComponentName: context.source.componentName,
|
|
1566
|
+
targetComponentId: context.target?.componentId ?? null,
|
|
1567
|
+
targetComponentName: context.target?.componentName ?? null,
|
|
1568
|
+
importSpecifier: context.importSpecifier,
|
|
1569
|
+
resolvedTargetPath: context.resolvedTargetPath ?? null,
|
|
1570
|
+
isExternal: context.isExternal,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function resolveImportDependencyContextMetadata(architectureContract, dependencyImport, resolver = (0, architectureDependencyContext_1.createArchitectureDependencyContextResolver)(architectureContract)) {
|
|
1574
|
+
const directContext = resolver.resolve({
|
|
1575
|
+
sourceFile: dependencyImport.sourceFileRelativePath,
|
|
1576
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
1577
|
+
});
|
|
1578
|
+
if (!directContext.isExternal) {
|
|
1579
|
+
return toDependencyContextMetadata(directContext);
|
|
1580
|
+
}
|
|
1581
|
+
const moduleTargetContext = resolver.resolve({
|
|
1582
|
+
sourceFile: dependencyImport.sourceFileRelativePath,
|
|
1583
|
+
importSpecifier: dependencyImport.targetModule,
|
|
1584
|
+
});
|
|
1585
|
+
if (!moduleTargetContext.isExternal) {
|
|
1586
|
+
return toDependencyContextMetadata({
|
|
1587
|
+
...moduleTargetContext,
|
|
1588
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
1589
|
+
resolvedTargetPath: dependencyImport.resolvedTargetRelativePath,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
const resolvedPathContext = resolver.resolve({
|
|
1593
|
+
sourceFile: dependencyImport.sourceFileRelativePath,
|
|
1594
|
+
importSpecifier: dependencyImport.resolvedTargetRelativePath,
|
|
1595
|
+
});
|
|
1596
|
+
return toDependencyContextMetadata(resolvedPathContext.isExternal
|
|
1597
|
+
? directContext
|
|
1598
|
+
: {
|
|
1599
|
+
...resolvedPathContext,
|
|
1600
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
1601
|
+
resolvedTargetPath: dependencyImport.resolvedTargetRelativePath,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
function buildComponentDependencyPolicyMap(architectureContract) {
|
|
1605
|
+
if (!architectureContract?.components || Object.keys(architectureContract.components).length === 0) {
|
|
1606
|
+
return new Map();
|
|
1607
|
+
}
|
|
1608
|
+
const entries = Array.isArray(architectureContract.components)
|
|
1609
|
+
? architectureContract.components.map((component, index) => [
|
|
1610
|
+
component.id ?? `component-${index + 1}`,
|
|
1611
|
+
component,
|
|
1612
|
+
])
|
|
1613
|
+
: Object.entries(architectureContract.components).sort(([left], [right]) => left.localeCompare(right));
|
|
1614
|
+
const policies = new Map();
|
|
1615
|
+
for (const [componentId, component] of entries) {
|
|
1616
|
+
if (component.allowedDependencies === undefined) {
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
policies.set(componentId, [...component.allowedDependencies].sort((left, right) => left.localeCompare(right)));
|
|
1620
|
+
}
|
|
1621
|
+
return policies;
|
|
1622
|
+
}
|
|
1623
|
+
function validateComponentDependencyPolicies(architectureContract, imports) {
|
|
1624
|
+
const policies = buildComponentDependencyPolicyMap(architectureContract);
|
|
1625
|
+
if (!architectureContract || policies.size === 0) {
|
|
1626
|
+
return [];
|
|
1627
|
+
}
|
|
1628
|
+
const dependencyContextResolver = (0, architectureDependencyContext_1.createArchitectureDependencyContextResolver)(architectureContract);
|
|
1629
|
+
const findings = [];
|
|
1630
|
+
for (const dependencyImport of imports) {
|
|
1631
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(architectureContract, dependencyImport, dependencyContextResolver);
|
|
1632
|
+
if (dependencyMetadata.dependencyType !== 'cross-component' ||
|
|
1633
|
+
dependencyMetadata.isExternal ||
|
|
1634
|
+
!dependencyMetadata.sourceComponentId ||
|
|
1635
|
+
!dependencyMetadata.targetComponentId) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const allowedDependencies = policies.get(dependencyMetadata.sourceComponentId);
|
|
1639
|
+
if (allowedDependencies === undefined) {
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (allowedDependencies.includes(dependencyMetadata.targetComponentId)) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
findings.push({
|
|
1646
|
+
result: makeValidationResult('AP-DEP-011', 'error', false, 'Disallowed cross-component dependency', {
|
|
1647
|
+
findingType: 'module-dependency',
|
|
1648
|
+
module: dependencyImport.sourceModule,
|
|
1649
|
+
target: dependencyImport.targetModule,
|
|
1650
|
+
...dependencyMetadata,
|
|
1651
|
+
}),
|
|
1652
|
+
filePath: dependencyImport.sourceFileRelativePath,
|
|
1653
|
+
line: dependencyImport.line,
|
|
1654
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
1655
|
+
findingType: 'module-dependency',
|
|
1656
|
+
module: dependencyImport.sourceModule,
|
|
1657
|
+
target: dependencyImport.targetModule,
|
|
1658
|
+
...dependencyMetadata,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
return findings;
|
|
1662
|
+
}
|
|
1663
|
+
function isModuleScopedDependencyImport(architectureContract, dependencyImport, resolver) {
|
|
1664
|
+
if (dependencyImport.sourceModule === dependencyImport.targetModule ||
|
|
1665
|
+
isParentChildModuleRelationship(dependencyImport.sourceModule, dependencyImport.targetModule)) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
if (!architectureContract) {
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(architectureContract, dependencyImport, resolver);
|
|
1672
|
+
// Module and component boundaries are independent axes. A real cross-module
|
|
1673
|
+
// import can also be cross-component, and must still count for module contracts.
|
|
1674
|
+
return !dependencyMetadata.isExternal;
|
|
1675
|
+
}
|
|
1676
|
+
function buildDependencyConfigSkippedFindings(reason, options) {
|
|
1677
|
+
const findings = [];
|
|
1678
|
+
if (options.checkForbiddenDependencies) {
|
|
1679
|
+
findings.push({
|
|
1680
|
+
result: makeValidationResult('AP-DEP-002', 'info', false, `Forbidden cross-module dependency validation skipped: ${reason}`),
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
if (options.checkNonPublicImports) {
|
|
1684
|
+
findings.push({
|
|
1685
|
+
result: makeValidationResult('AP-DEP-003', 'info', false, `Non-public cross-module import validation skipped: ${reason}`),
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
return findings;
|
|
1689
|
+
}
|
|
1690
|
+
function normalizePathList(values) {
|
|
1691
|
+
return values.map((value) => normalizePath(value)).sort((left, right) => left.localeCompare(right));
|
|
1692
|
+
}
|
|
1693
|
+
function normalizeModuleContract(contractLike) {
|
|
1694
|
+
if (typeof contractLike.module !== 'string') {
|
|
1695
|
+
return undefined;
|
|
1696
|
+
}
|
|
1697
|
+
const dependsOn = Array.isArray(contractLike.dependsOn)
|
|
1698
|
+
? contractLike.dependsOn.filter((item) => typeof item === 'string')
|
|
1699
|
+
: [];
|
|
1700
|
+
const publicEntrypoints = Array.isArray(contractLike.publicEntrypoints)
|
|
1701
|
+
? contractLike.publicEntrypoints.filter((item) => typeof item === 'string')
|
|
1702
|
+
: [];
|
|
1703
|
+
const ownedTables = Array.isArray(contractLike.ownedTables)
|
|
1704
|
+
? contractLike.ownedTables.filter((item) => typeof item === 'string')
|
|
1705
|
+
: [];
|
|
1706
|
+
const exposedApis = Array.isArray(contractLike.exposedApis)
|
|
1707
|
+
? contractLike.exposedApis.filter((item) => typeof item === 'string')
|
|
1708
|
+
: [];
|
|
1709
|
+
if (dependsOn.length !== (contractLike.dependsOn?.length ?? 0) ||
|
|
1710
|
+
publicEntrypoints.length !== (contractLike.publicEntrypoints?.length ?? 0) ||
|
|
1711
|
+
ownedTables.length !== (contractLike.ownedTables?.length ?? 0) ||
|
|
1712
|
+
exposedApis.length !== (contractLike.exposedApis?.length ?? 0)) {
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
module: contractLike.module,
|
|
1717
|
+
dependsOn,
|
|
1718
|
+
publicEntrypoints,
|
|
1719
|
+
ownedTables,
|
|
1720
|
+
exposedApis,
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
async function loadModuleArchitectureContract(workspaceRoot, architectureContract, moduleName, cache) {
|
|
1724
|
+
const cached = cache.get(moduleName);
|
|
1725
|
+
if (cached) {
|
|
1726
|
+
return cached;
|
|
1727
|
+
}
|
|
1728
|
+
const registryEntry = architectureContract.modules[moduleName];
|
|
1729
|
+
const contractPath = registryEntry?.contract ??
|
|
1730
|
+
path.posix.join((0, moduleDiscovery_1.resolveContractsRootFromContract)(architectureContract), `${toSafeContractModuleFileStem(moduleName)}.contract.json`);
|
|
1731
|
+
const fullContractPath = path.join(workspaceRoot, ...contractPath.split('/'));
|
|
1732
|
+
const content = await readTextFileIfExists(fullContractPath);
|
|
1733
|
+
if (!content) {
|
|
1734
|
+
const missingResult = {
|
|
1735
|
+
status: 'missing',
|
|
1736
|
+
contractPath,
|
|
1737
|
+
reason: 'missing_file',
|
|
1738
|
+
};
|
|
1739
|
+
cache.set(moduleName, missingResult);
|
|
1740
|
+
return missingResult;
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const parsed = JSON.parse(content);
|
|
1744
|
+
const normalized = normalizeModuleContract(parsed);
|
|
1745
|
+
if (!normalized) {
|
|
1746
|
+
const invalidResult = {
|
|
1747
|
+
status: 'invalid',
|
|
1748
|
+
contractPath,
|
|
1749
|
+
reason: 'invalid_shape',
|
|
1750
|
+
};
|
|
1751
|
+
cache.set(moduleName, invalidResult);
|
|
1752
|
+
return invalidResult;
|
|
1753
|
+
}
|
|
1754
|
+
const okResult = {
|
|
1755
|
+
status: 'ok',
|
|
1756
|
+
contract: normalized,
|
|
1757
|
+
contractPath,
|
|
1758
|
+
};
|
|
1759
|
+
cache.set(moduleName, okResult);
|
|
1760
|
+
return okResult;
|
|
1761
|
+
}
|
|
1762
|
+
catch {
|
|
1763
|
+
const invalidResult = {
|
|
1764
|
+
status: 'invalid',
|
|
1765
|
+
contractPath,
|
|
1766
|
+
reason: 'invalid_json',
|
|
1767
|
+
};
|
|
1768
|
+
cache.set(moduleName, invalidResult);
|
|
1769
|
+
return invalidResult;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function describeModuleContractStatus(role, moduleName, loadResult) {
|
|
1773
|
+
const contractLocation = loadResult.contractPath ??
|
|
1774
|
+
(loadResult.reason === 'missing_reference'
|
|
1775
|
+
? 'no contract path is registered in architecture.json'
|
|
1776
|
+
: 'unknown path');
|
|
1777
|
+
if (loadResult.status === 'missing') {
|
|
1778
|
+
return `${role} module contract for '${moduleName}' is missing (${contractLocation})`;
|
|
1779
|
+
}
|
|
1780
|
+
return `${role} module contract for '${moduleName}' is invalid (${contractLocation})`;
|
|
1781
|
+
}
|
|
1782
|
+
function buildContractSkipMessage(ruleId, sourceModule, targetModule, sourceContract, targetContract) {
|
|
1783
|
+
const ruleLabel = ruleId === 'AP-DEP-004'
|
|
1784
|
+
? 'Dependency declaration validation'
|
|
1785
|
+
: 'Public surface validation';
|
|
1786
|
+
const sourceUnavailable = sourceContract.status !== 'ok';
|
|
1787
|
+
const targetUnavailable = targetContract.status !== 'ok';
|
|
1788
|
+
if (sourceUnavailable && targetUnavailable) {
|
|
1789
|
+
return `${ruleLabel} skipped for ${ruleId} because ${describeModuleContractStatus('source', sourceModule, sourceContract)} and ${describeModuleContractStatus('target', targetModule, targetContract)}.`;
|
|
1790
|
+
}
|
|
1791
|
+
if (sourceUnavailable) {
|
|
1792
|
+
return `${ruleLabel} skipped for ${ruleId} because ${describeModuleContractStatus('source', sourceModule, sourceContract)}.`;
|
|
1793
|
+
}
|
|
1794
|
+
return `${ruleLabel} skipped for ${ruleId} because ${describeModuleContractStatus('target', targetModule, targetContract)}.`;
|
|
1795
|
+
}
|
|
1796
|
+
function isImportWithinPublicEntrypoints(resolvedTargetRelativePath, publicEntrypoints) {
|
|
1797
|
+
const normalizedTargetPath = normalizePath(resolvedTargetRelativePath);
|
|
1798
|
+
for (const publicEntrypoint of normalizePathList(publicEntrypoints)) {
|
|
1799
|
+
const isFileEntrypoint = /\.[a-z0-9]+$/iu.test(publicEntrypoint);
|
|
1800
|
+
if (isFileEntrypoint && normalizedTargetPath === publicEntrypoint) {
|
|
1801
|
+
return true;
|
|
1802
|
+
}
|
|
1803
|
+
if (!isFileEntrypoint) {
|
|
1804
|
+
if (normalizedTargetPath === publicEntrypoint ||
|
|
1805
|
+
normalizedTargetPath.startsWith(`${publicEntrypoint}/`)) {
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
async function filterExistingPublicEntrypoints(workspaceRoot, publicEntrypoints) {
|
|
1813
|
+
const existingEntrypoints = [];
|
|
1814
|
+
for (const publicEntrypoint of normalizePathList([...publicEntrypoints])) {
|
|
1815
|
+
const absolutePath = path.join(workspaceRoot, ...publicEntrypoint.split('/'));
|
|
1816
|
+
if (await pathExists(absolutePath)) {
|
|
1817
|
+
existingEntrypoints.push(publicEntrypoint);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return existingEntrypoints;
|
|
1821
|
+
}
|
|
1822
|
+
async function inferExistingPublicEntrypointsFromModuleRoot(workspaceRoot, moduleRoot) {
|
|
1823
|
+
if (!moduleRoot) {
|
|
1824
|
+
return [];
|
|
1825
|
+
}
|
|
1826
|
+
const candidateSubpaths = [
|
|
1827
|
+
'index.ts',
|
|
1828
|
+
'index.tsx',
|
|
1829
|
+
'index.js',
|
|
1830
|
+
'index.jsx',
|
|
1831
|
+
'src/index.ts',
|
|
1832
|
+
'src/index.tsx',
|
|
1833
|
+
'src/index.js',
|
|
1834
|
+
'src/index.jsx',
|
|
1835
|
+
'public/index.ts',
|
|
1836
|
+
'public/index.tsx',
|
|
1837
|
+
'public/index.js',
|
|
1838
|
+
'public/index.jsx',
|
|
1839
|
+
];
|
|
1840
|
+
const existingEntrypoints = [];
|
|
1841
|
+
for (const candidateSubpath of candidateSubpaths) {
|
|
1842
|
+
const candidatePath = path.join(moduleRoot, ...candidateSubpath.split('/'));
|
|
1843
|
+
if (await pathExists(candidatePath)) {
|
|
1844
|
+
existingEntrypoints.push(normalizePath(path.relative(workspaceRoot, candidatePath)));
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return existingEntrypoints.sort((left, right) => left.localeCompare(right));
|
|
1848
|
+
}
|
|
1849
|
+
async function resolveModulePublicEntrypoints(workspaceRoot, architectureContract, moduleName, targetContract, moduleRoots) {
|
|
1850
|
+
const registryPublicEntrypoints = architectureContract.modules[moduleName]?.publicEntrypoints ?? [];
|
|
1851
|
+
const explicitPublicEntrypoints = sortUnique([
|
|
1852
|
+
...registryPublicEntrypoints,
|
|
1853
|
+
...targetContract.publicEntrypoints,
|
|
1854
|
+
]);
|
|
1855
|
+
const existingExplicitEntrypoints = await filterExistingPublicEntrypoints(workspaceRoot, explicitPublicEntrypoints);
|
|
1856
|
+
if (existingExplicitEntrypoints.length > 0) {
|
|
1857
|
+
return existingExplicitEntrypoints;
|
|
1858
|
+
}
|
|
1859
|
+
return inferExistingPublicEntrypointsFromModuleRoot(workspaceRoot, moduleRoots.get(moduleName));
|
|
1860
|
+
}
|
|
1861
|
+
async function resolveKnownModulePublicEntrypoints(workspaceRoot, architectureContract, moduleName, targetContract, moduleRoots, adapter) {
|
|
1862
|
+
const registryPublicEntrypoints = architectureContract?.modules[moduleName]?.publicEntrypoints ?? [];
|
|
1863
|
+
const contractPublicEntrypoints = targetContract?.publicEntrypoints ?? [];
|
|
1864
|
+
const explicitPublicEntrypoints = sortUnique([
|
|
1865
|
+
...registryPublicEntrypoints,
|
|
1866
|
+
...contractPublicEntrypoints,
|
|
1867
|
+
]);
|
|
1868
|
+
const existingExplicitEntrypoints = await filterExistingPublicEntrypoints(workspaceRoot, explicitPublicEntrypoints);
|
|
1869
|
+
if (existingExplicitEntrypoints.length > 0) {
|
|
1870
|
+
return existingExplicitEntrypoints;
|
|
1871
|
+
}
|
|
1872
|
+
if (usesJavaImportParser(adapter)) {
|
|
1873
|
+
return [];
|
|
1874
|
+
}
|
|
1875
|
+
return inferExistingPublicEntrypointsFromModuleRoot(workspaceRoot, moduleRoots.get(moduleName));
|
|
1876
|
+
}
|
|
1877
|
+
function labelPublicEntrypoint(publicEntrypoint) {
|
|
1878
|
+
const normalizedEntrypoint = normalizePath(publicEntrypoint);
|
|
1879
|
+
const fileName = normalizedEntrypoint.split('/').at(-1) ?? '';
|
|
1880
|
+
return /^index\.[cm]?[jt]sx?$/iu.test(fileName)
|
|
1881
|
+
? `${fileName} public entrypoint`
|
|
1882
|
+
: 'public entrypoint';
|
|
1883
|
+
}
|
|
1884
|
+
function describePublicEntrypointTargets(publicEntrypoints) {
|
|
1885
|
+
if (publicEntrypoints.length <= 1) {
|
|
1886
|
+
return publicEntrypoints[0] ?? 'unknown public entrypoint';
|
|
1887
|
+
}
|
|
1888
|
+
return publicEntrypoints.join(', ');
|
|
1889
|
+
}
|
|
1890
|
+
async function describeModulePublicImportGuidance(workspaceRoot, moduleName, moduleRoots) {
|
|
1891
|
+
const moduleRoot = moduleRoots.get(moduleName);
|
|
1892
|
+
const inferredEntrypoints = await inferExistingPublicEntrypointsFromModuleRoot(workspaceRoot, moduleRoot);
|
|
1893
|
+
if (inferredEntrypoints.length > 0) {
|
|
1894
|
+
return `Use the public entrypoint at ${describePublicEntrypointTargets(inferredEntrypoints)} instead.`;
|
|
1895
|
+
}
|
|
1896
|
+
if (moduleRoot) {
|
|
1897
|
+
return `Use an existing public entrypoint under ${normalizePath(path.relative(workspaceRoot, moduleRoot))} instead.`;
|
|
1898
|
+
}
|
|
1899
|
+
return 'Use an existing target module public entrypoint instead.';
|
|
1900
|
+
}
|
|
1901
|
+
function buildActualImportedModuleSetBySource(imports, registeredModuleIds) {
|
|
1902
|
+
const bySource = new Map();
|
|
1903
|
+
for (const dependencyImport of imports) {
|
|
1904
|
+
if (!registeredModuleIds.has(dependencyImport.sourceModule)) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if (!registeredModuleIds.has(dependencyImport.targetModule)) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const existing = bySource.get(dependencyImport.sourceModule);
|
|
1911
|
+
if (existing) {
|
|
1912
|
+
existing.add(dependencyImport.targetModule);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
bySource.set(dependencyImport.sourceModule, new Set([dependencyImport.targetModule]));
|
|
1916
|
+
}
|
|
1917
|
+
return new Map([...bySource.entries()]
|
|
1918
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1919
|
+
.map(([sourceModule, targetModules]) => [
|
|
1920
|
+
sourceModule,
|
|
1921
|
+
new Set([...targetModules].sort((left, right) => left.localeCompare(right))),
|
|
1922
|
+
]));
|
|
1923
|
+
}
|
|
1924
|
+
function sortUnique(values) {
|
|
1925
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
1926
|
+
}
|
|
1927
|
+
function summarizeValues(values, limit) {
|
|
1928
|
+
const visible = values.slice(0, limit);
|
|
1929
|
+
const remaining = values.length - visible.length;
|
|
1930
|
+
return remaining > 0 ? `${visible.join(', ')} (+${remaining} more)` : visible.join(', ');
|
|
1931
|
+
}
|
|
1932
|
+
function buildInboundDependencyMap(bySource, moduleIds) {
|
|
1933
|
+
const inbound = new Map();
|
|
1934
|
+
for (const moduleId of moduleIds) {
|
|
1935
|
+
inbound.set(moduleId, new Set());
|
|
1936
|
+
}
|
|
1937
|
+
for (const [source, targets] of bySource.entries()) {
|
|
1938
|
+
for (const target of targets) {
|
|
1939
|
+
const existing = inbound.get(target);
|
|
1940
|
+
if (!existing) {
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
existing.add(source);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return new Map(moduleIds.map((moduleId) => [
|
|
1947
|
+
moduleId,
|
|
1948
|
+
sortUnique(inbound.get(moduleId) ?? []),
|
|
1949
|
+
]));
|
|
1950
|
+
}
|
|
1951
|
+
async function buildDependencyGraphSummary(workspaceRoot, architectureContract, options) {
|
|
1952
|
+
const modulesRoot = options?.modulesRootRelativePath ??
|
|
1953
|
+
(0, moduleDiscovery_1.resolveModulesRootFromContract)(architectureContract);
|
|
1954
|
+
const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(architectureContract);
|
|
1955
|
+
const moduleRoots = await findModuleRoots(workspaceRoot, modulesRoot, architectureContract, { canonicalizeOverlappingPaths: options?.canonicalizeOverlappingModulePaths === true });
|
|
1956
|
+
const explicitStandaloneModuleByPath = await buildExplicitStandaloneModulePathMap(workspaceRoot, architectureContract);
|
|
1957
|
+
const workspacePackageAliases = await buildWorkspacePackageAliasMap(workspaceRoot, moduleRoots);
|
|
1958
|
+
const moduleIds = sortUnique([...moduleRoots.keys(), ...explicitStandaloneModuleByPath.values()]);
|
|
1959
|
+
const registeredModuleIds = new Set(moduleIds);
|
|
1960
|
+
const resolutionContext = createDependencyGraphResolutionContext();
|
|
1961
|
+
const imports = await collectCrossModuleImports(workspaceRoot, modulesRoot, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, adapter, { resolutionContext });
|
|
1962
|
+
options?.onCollectedImports?.(imports);
|
|
1963
|
+
options?.onResolutionStats?.(resolutionContext.getStats());
|
|
1964
|
+
const peerImports = filterPeerModuleImports(imports);
|
|
1965
|
+
const moduleScopedPeerImports = peerImports.filter((dependencyImport) => isModuleScopedDependencyImport(architectureContract, dependencyImport));
|
|
1966
|
+
const actualBySource = buildActualImportedModuleSetBySource(moduleScopedPeerImports, registeredModuleIds);
|
|
1967
|
+
const moduleContractCache = new Map();
|
|
1968
|
+
const declaredBySource = new Map();
|
|
1969
|
+
const hasContractByModule = new Map();
|
|
1970
|
+
for (const moduleId of moduleIds) {
|
|
1971
|
+
const sourceContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, moduleId, moduleContractCache);
|
|
1972
|
+
if (sourceContract.status === 'ok' && sourceContract.contract) {
|
|
1973
|
+
hasContractByModule.set(moduleId, true);
|
|
1974
|
+
declaredBySource.set(moduleId, new Set(sortUnique(sourceContract.contract.dependsOn.filter((dependencyModule) => registeredModuleIds.has(dependencyModule) &&
|
|
1975
|
+
!isParentChildModuleRelationship(moduleId, dependencyModule)))));
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
hasContractByModule.set(moduleId, false);
|
|
1979
|
+
declaredBySource.set(moduleId, new Set());
|
|
1980
|
+
}
|
|
1981
|
+
const inboundDeclared = buildInboundDependencyMap(declaredBySource, moduleIds);
|
|
1982
|
+
const inboundActual = buildInboundDependencyMap(actualBySource, moduleIds);
|
|
1983
|
+
const hierarchyEdges = buildModuleHierarchyEdges(moduleIds);
|
|
1984
|
+
const parentModuleByChild = new Map(hierarchyEdges.map((edge) => [edge.childModule, edge.parentModule]));
|
|
1985
|
+
const moduleSummaries = moduleIds.map((moduleId) => {
|
|
1986
|
+
const declaredDependencies = sortUnique(declaredBySource.get(moduleId) ?? []);
|
|
1987
|
+
const actualDependencies = sortUnique(actualBySource.get(moduleId) ?? []);
|
|
1988
|
+
const unusedDeclaredDependencies = declaredDependencies.filter((declaredDependency) => !actualDependencies.includes(declaredDependency));
|
|
1989
|
+
const undeclaredActualDependencies = actualDependencies.filter((actualDependency) => !declaredDependencies.includes(actualDependency));
|
|
1990
|
+
return {
|
|
1991
|
+
module: moduleId,
|
|
1992
|
+
hasContract: hasContractByModule.get(moduleId) ?? false,
|
|
1993
|
+
...(parentModuleByChild.has(moduleId)
|
|
1994
|
+
? { parentModule: parentModuleByChild.get(moduleId) }
|
|
1995
|
+
: {}),
|
|
1996
|
+
childModules: [],
|
|
1997
|
+
declaredDependencies,
|
|
1998
|
+
actualDependencies,
|
|
1999
|
+
inboundDeclaredFrom: inboundDeclared.get(moduleId) ?? [],
|
|
2000
|
+
inboundActualFrom: inboundActual.get(moduleId) ?? [],
|
|
2001
|
+
unusedDeclaredDependencies,
|
|
2002
|
+
undeclaredActualDependencies,
|
|
2003
|
+
};
|
|
2004
|
+
}).filter((moduleSummary) => !isIsolatedContractlessHierarchyModule(moduleSummary));
|
|
2005
|
+
const retainedModuleIds = new Set(moduleSummaries.map((moduleSummary) => moduleSummary.module));
|
|
2006
|
+
const retainedHierarchyEdges = hierarchyEdges.filter((edge) => retainedModuleIds.has(edge.parentModule) && retainedModuleIds.has(edge.childModule));
|
|
2007
|
+
const childModulesByParent = buildChildModulesByParent(retainedHierarchyEdges);
|
|
2008
|
+
return {
|
|
2009
|
+
modules: moduleSummaries.map((moduleSummary) => ({
|
|
2010
|
+
...moduleSummary,
|
|
2011
|
+
childModules: childModulesByParent.get(moduleSummary.module) ?? [],
|
|
2012
|
+
})),
|
|
2013
|
+
hierarchyEdges: retainedHierarchyEdges,
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
async function validateDependencyContractBoundaries(workspaceRoot, architectureContract, options) {
|
|
2017
|
+
if (!options.checkDeclaredDependencies &&
|
|
2018
|
+
!options.checkPublicSurfaceImports &&
|
|
2019
|
+
!options.checkDeclaredDependenciesUsedByCode) {
|
|
2020
|
+
return [];
|
|
2021
|
+
}
|
|
2022
|
+
const modulesRoot = options.modulesRootRelativePath ??
|
|
2023
|
+
(0, moduleDiscovery_1.resolveModulesRootFromContract)(architectureContract);
|
|
2024
|
+
const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(architectureContract);
|
|
2025
|
+
const moduleRoots = await findModuleRoots(workspaceRoot, modulesRoot, architectureContract);
|
|
2026
|
+
const explicitStandaloneModuleByPath = await buildExplicitStandaloneModulePathMap(workspaceRoot, architectureContract);
|
|
2027
|
+
const workspacePackageAliases = await buildWorkspacePackageAliasMap(workspaceRoot, moduleRoots);
|
|
2028
|
+
const imports = options.precomputedImports
|
|
2029
|
+
? [...options.precomputedImports]
|
|
2030
|
+
: await collectCrossModuleImports(workspaceRoot, modulesRoot, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, adapter);
|
|
2031
|
+
const dependencyContextResolver = (0, architectureDependencyContext_1.createArchitectureDependencyContextResolver)(architectureContract);
|
|
2032
|
+
const peerImports = filterPeerModuleImports(imports);
|
|
2033
|
+
const moduleScopedPeerImports = peerImports.filter((dependencyImport) => isModuleScopedDependencyImport(architectureContract, dependencyImport, dependencyContextResolver));
|
|
2034
|
+
const findings = [];
|
|
2035
|
+
const moduleContractCache = new Map();
|
|
2036
|
+
if (options.checkDeclaredDependencies) {
|
|
2037
|
+
const failuresByDependency = new Map();
|
|
2038
|
+
const skippedFindings = new Map();
|
|
2039
|
+
for (const dependencyImport of moduleScopedPeerImports) {
|
|
2040
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(architectureContract, dependencyImport, dependencyContextResolver);
|
|
2041
|
+
if (dependencyMetadata.isExternal) {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const sourceContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, dependencyImport.sourceModule, moduleContractCache);
|
|
2045
|
+
const targetContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, dependencyImport.targetModule, moduleContractCache);
|
|
2046
|
+
if (sourceContract.status !== 'ok' || !sourceContract.contract) {
|
|
2047
|
+
const skipKey = [
|
|
2048
|
+
'AP-DEP-004',
|
|
2049
|
+
'source',
|
|
2050
|
+
dependencyImport.sourceModule,
|
|
2051
|
+
sourceContract.status,
|
|
2052
|
+
sourceContract.contractPath ?? '',
|
|
2053
|
+
].join('|');
|
|
2054
|
+
if (!skippedFindings.has(skipKey)) {
|
|
2055
|
+
skippedFindings.set(skipKey, {
|
|
2056
|
+
result: makeValidationResult('AP-DEP-004', 'info', false, buildContractSkipMessage('AP-DEP-004', dependencyImport.sourceModule, dependencyImport.targetModule, sourceContract, targetContract), {
|
|
2057
|
+
findingType: 'module-dependency',
|
|
2058
|
+
module: dependencyImport.sourceModule,
|
|
2059
|
+
target: dependencyImport.targetModule,
|
|
2060
|
+
...dependencyMetadata,
|
|
2061
|
+
}),
|
|
2062
|
+
filePath: dependencyImport.sourceFileRelativePath,
|
|
2063
|
+
line: dependencyImport.line,
|
|
2064
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
2065
|
+
findingType: 'module-dependency',
|
|
2066
|
+
module: dependencyImport.sourceModule,
|
|
2067
|
+
target: dependencyImport.targetModule,
|
|
2068
|
+
...dependencyMetadata,
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
if (!sourceContract.contract.dependsOn.includes(dependencyImport.targetModule)) {
|
|
2074
|
+
const dependencyKey = `${dependencyImport.sourceModule}->${dependencyImport.targetModule}`;
|
|
2075
|
+
const existing = failuresByDependency.get(dependencyKey);
|
|
2076
|
+
if (existing) {
|
|
2077
|
+
existing.imports.push(dependencyImport);
|
|
2078
|
+
}
|
|
2079
|
+
else {
|
|
2080
|
+
failuresByDependency.set(dependencyKey, {
|
|
2081
|
+
dependencyImport,
|
|
2082
|
+
metadata: dependencyMetadata,
|
|
2083
|
+
imports: [dependencyImport],
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
const failures = [...failuresByDependency.values()]
|
|
2089
|
+
.sort((left, right) => {
|
|
2090
|
+
const sourceCompare = left.dependencyImport.sourceModule.localeCompare(right.dependencyImport.sourceModule);
|
|
2091
|
+
if (sourceCompare !== 0) {
|
|
2092
|
+
return sourceCompare;
|
|
2093
|
+
}
|
|
2094
|
+
return left.dependencyImport.targetModule.localeCompare(right.dependencyImport.targetModule);
|
|
2095
|
+
})
|
|
2096
|
+
.map(({ dependencyImport, imports, metadata }) => {
|
|
2097
|
+
const representativeFiles = sortUnique(imports.map((entry) => entry.sourceFileRelativePath));
|
|
2098
|
+
const representativeImports = sortUnique(imports.map((entry) => entry.importSpecifier));
|
|
2099
|
+
const fileSummary = summarizeValues(representativeFiles, 3);
|
|
2100
|
+
const importSummary = summarizeValues(representativeImports, 3);
|
|
2101
|
+
return {
|
|
2102
|
+
result: makeValidationResult('AP-DEP-004', 'error', false, `Module '${dependencyImport.sourceModule}' imports module '${dependencyImport.targetModule}' but '${dependencyImport.targetModule}' is not declared in ${dependencyImport.sourceModule}.contract.json dependsOn. Import count: ${imports.length}. Representative files: ${fileSummary}. Representative imports: ${importSummary}.`, {
|
|
2103
|
+
findingType: 'module-dependency',
|
|
2104
|
+
module: dependencyImport.sourceModule,
|
|
2105
|
+
target: dependencyImport.targetModule,
|
|
2106
|
+
...metadata,
|
|
2107
|
+
}),
|
|
2108
|
+
filePath: dependencyImport.sourceFileRelativePath,
|
|
2109
|
+
line: dependencyImport.line,
|
|
2110
|
+
findingType: 'module-dependency',
|
|
2111
|
+
module: dependencyImport.sourceModule,
|
|
2112
|
+
target: dependencyImport.targetModule,
|
|
2113
|
+
...metadata,
|
|
2114
|
+
};
|
|
2115
|
+
});
|
|
2116
|
+
if (failures.length > 0) {
|
|
2117
|
+
findings.push(...failures);
|
|
2118
|
+
}
|
|
2119
|
+
else if (moduleScopedPeerImports.length === 0) {
|
|
2120
|
+
findings.push({
|
|
2121
|
+
result: makeValidationResult('AP-DEP-004', 'error', true, 'No cross-module imports detected for contract dependency declaration checks.'),
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
else if (skippedFindings.size > 0) {
|
|
2125
|
+
findings.push(...[...skippedFindings.values()]);
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
findings.push({
|
|
2129
|
+
result: makeValidationResult('AP-DEP-004', 'error', true, 'All cross-module imports are declared in source module contract dependsOn lists.'),
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (options.checkPublicSurfaceImports) {
|
|
2134
|
+
const failures = [];
|
|
2135
|
+
const skippedFindings = new Map();
|
|
2136
|
+
for (const dependencyImport of moduleScopedPeerImports) {
|
|
2137
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(architectureContract, dependencyImport, dependencyContextResolver);
|
|
2138
|
+
if (dependencyMetadata.isExternal) {
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
const sourceContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, dependencyImport.sourceModule, moduleContractCache);
|
|
2142
|
+
const targetContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, dependencyImport.targetModule, moduleContractCache);
|
|
2143
|
+
if (targetContract.status !== 'ok' || !targetContract.contract) {
|
|
2144
|
+
const unavailableRoles = [];
|
|
2145
|
+
if (sourceContract.status !== 'ok') {
|
|
2146
|
+
unavailableRoles.push({
|
|
2147
|
+
role: 'source',
|
|
2148
|
+
moduleName: dependencyImport.sourceModule,
|
|
2149
|
+
contract: sourceContract,
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
if (targetContract.status !== 'ok') {
|
|
2153
|
+
unavailableRoles.push({
|
|
2154
|
+
role: 'target',
|
|
2155
|
+
moduleName: dependencyImport.targetModule,
|
|
2156
|
+
contract: targetContract,
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
for (const unavailable of unavailableRoles) {
|
|
2160
|
+
const skipKey = [
|
|
2161
|
+
'AP-DEP-005',
|
|
2162
|
+
unavailable.role,
|
|
2163
|
+
unavailable.moduleName,
|
|
2164
|
+
unavailable.contract.status,
|
|
2165
|
+
unavailable.contract.contractPath ?? '',
|
|
2166
|
+
].join('|');
|
|
2167
|
+
if (skippedFindings.has(skipKey)) {
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
skippedFindings.set(skipKey, {
|
|
2171
|
+
result: makeValidationResult('AP-DEP-005', 'info', false, buildContractSkipMessage('AP-DEP-005', dependencyImport.sourceModule, dependencyImport.targetModule, sourceContract, targetContract), {
|
|
2172
|
+
findingType: 'module-dependency',
|
|
2173
|
+
module: dependencyImport.sourceModule,
|
|
2174
|
+
target: dependencyImport.targetModule,
|
|
2175
|
+
...dependencyMetadata,
|
|
2176
|
+
}),
|
|
2177
|
+
filePath: dependencyImport.sourceFileRelativePath,
|
|
2178
|
+
line: dependencyImport.line,
|
|
2179
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
2180
|
+
findingType: 'module-dependency',
|
|
2181
|
+
module: dependencyImport.sourceModule,
|
|
2182
|
+
target: dependencyImport.targetModule,
|
|
2183
|
+
...dependencyMetadata,
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
const publicEntrypoints = await resolveKnownModulePublicEntrypoints(workspaceRoot, architectureContract, dependencyImport.targetModule, targetContract.contract, moduleRoots, adapter);
|
|
2189
|
+
if (publicEntrypoints.length === 0) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
if (options.suppressPublicSurfaceFailuresForNonPublicImports === true &&
|
|
2193
|
+
!dependencyImport.isPublicImport) {
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
if (dependencyImport.isPackagePublicImport === true) {
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
if (!isImportWithinPublicEntrypoints(dependencyImport.resolvedTargetRelativePath, publicEntrypoints)) {
|
|
2200
|
+
const publicEntrypointLabel = labelPublicEntrypoint(publicEntrypoints[0] ?? '');
|
|
2201
|
+
const expectedEntrypoint = describePublicEntrypointTargets(publicEntrypoints);
|
|
2202
|
+
failures.push({
|
|
2203
|
+
result: makeValidationResult('AP-DEP-005', 'error', false, `Module '${dependencyImport.sourceModule}' must import module '${dependencyImport.targetModule}' via its ${publicEntrypointLabel}: ${expectedEntrypoint}.`, {
|
|
2204
|
+
findingType: 'module-dependency',
|
|
2205
|
+
module: dependencyImport.sourceModule,
|
|
2206
|
+
target: dependencyImport.targetModule,
|
|
2207
|
+
...dependencyMetadata,
|
|
2208
|
+
}),
|
|
2209
|
+
filePath: dependencyImport.sourceFileRelativePath,
|
|
2210
|
+
line: dependencyImport.line,
|
|
2211
|
+
importSpecifier: dependencyImport.importSpecifier,
|
|
2212
|
+
findingType: 'module-dependency',
|
|
2213
|
+
module: dependencyImport.sourceModule,
|
|
2214
|
+
target: dependencyImport.targetModule,
|
|
2215
|
+
...dependencyMetadata,
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (failures.length > 0) {
|
|
2220
|
+
findings.push(...failures);
|
|
2221
|
+
}
|
|
2222
|
+
else if (moduleScopedPeerImports.length === 0) {
|
|
2223
|
+
findings.push({
|
|
2224
|
+
result: makeValidationResult('AP-DEP-005', 'error', true, 'No cross-module imports detected for public-surface contract checks.'),
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
else if (skippedFindings.size > 0) {
|
|
2228
|
+
findings.push(...[...skippedFindings.values()]);
|
|
2229
|
+
}
|
|
2230
|
+
else {
|
|
2231
|
+
findings.push({
|
|
2232
|
+
result: makeValidationResult('AP-DEP-005', 'error', true, 'All cross-module imports resolve through target module contract public entrypoints.'),
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (options.checkDeclaredDependenciesUsedByCode) {
|
|
2237
|
+
const failures = [];
|
|
2238
|
+
const canonicalModuleRoots = await findModuleRoots(workspaceRoot, modulesRoot, architectureContract, { canonicalizeOverlappingPaths: true });
|
|
2239
|
+
const canonicalImports = options.precomputedImports
|
|
2240
|
+
? [...options.precomputedImports]
|
|
2241
|
+
: await collectCrossModuleImports(workspaceRoot, modulesRoot, canonicalModuleRoots, explicitStandaloneModuleByPath, await buildWorkspacePackageAliasMap(workspaceRoot, canonicalModuleRoots), adapter);
|
|
2242
|
+
const canonicalPeerImports = filterPeerModuleImports(canonicalImports).filter((dependencyImport) => isModuleScopedDependencyImport(architectureContract, dependencyImport));
|
|
2243
|
+
const registeredModuleIds = new Set([
|
|
2244
|
+
...canonicalModuleRoots.keys(),
|
|
2245
|
+
...explicitStandaloneModuleByPath.values(),
|
|
2246
|
+
]);
|
|
2247
|
+
const importedBySource = buildActualImportedModuleSetBySource(canonicalPeerImports, registeredModuleIds);
|
|
2248
|
+
for (const moduleName of [...registeredModuleIds].sort((left, right) => left.localeCompare(right))) {
|
|
2249
|
+
const sourceContract = await loadModuleArchitectureContract(workspaceRoot, architectureContract, moduleName, moduleContractCache);
|
|
2250
|
+
if (sourceContract.status !== 'ok' || !sourceContract.contract) {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
const declaredDependencies = [...sourceContract.contract.dependsOn]
|
|
2254
|
+
.filter((dependencyModule) => registeredModuleIds.has(dependencyModule) &&
|
|
2255
|
+
!isParentChildModuleRelationship(moduleName, dependencyModule))
|
|
2256
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2257
|
+
if (declaredDependencies.length === 0) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
const actualImportedModules = importedBySource.get(moduleName) ?? new Set();
|
|
2261
|
+
for (const declaredDependency of declaredDependencies) {
|
|
2262
|
+
if (actualImportedModules.has(declaredDependency)) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
failures.push({
|
|
2266
|
+
result: makeValidationResult('AP-DEP-006', 'warning', false, `Module '${moduleName}' declares dependency on '${declaredDependency}' but no cross-module imports reference it.`, {
|
|
2267
|
+
findingType: 'module-dependency',
|
|
2268
|
+
module: moduleName,
|
|
2269
|
+
target: declaredDependency,
|
|
2270
|
+
}),
|
|
2271
|
+
...(sourceContract.contractPath
|
|
2272
|
+
? { filePath: sourceContract.contractPath }
|
|
2273
|
+
: {}),
|
|
2274
|
+
findingType: 'module-dependency',
|
|
2275
|
+
module: moduleName,
|
|
2276
|
+
target: declaredDependency,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (failures.length > 0) {
|
|
2281
|
+
findings.push(...failures);
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
findings.push({
|
|
2285
|
+
result: makeValidationResult('AP-DEP-006', 'warning', true, 'All declared module dependencies are referenced by cross-module imports.'),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return findings;
|
|
2290
|
+
}
|
|
2291
|
+
async function validateDependencyBoundaries(workspaceRoot, options) {
|
|
2292
|
+
if (!options.checkCircularDependencies &&
|
|
2293
|
+
!options.checkForbiddenDependencies &&
|
|
2294
|
+
!options.checkNonPublicImports &&
|
|
2295
|
+
options.checkComponentDependencyPolicies !== true) {
|
|
2296
|
+
return [];
|
|
2297
|
+
}
|
|
2298
|
+
const parsedArchitectureContract = await (0, architectureInit_1.loadArchitectureContract)(workspaceRoot);
|
|
2299
|
+
const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(parsedArchitectureContract);
|
|
2300
|
+
const modulesRootRelativePath = options.modulesRootRelativePath ??
|
|
2301
|
+
(0, moduleDiscovery_1.resolveModulesRootFromContract)(parsedArchitectureContract);
|
|
2302
|
+
const moduleRoots = await findModuleRoots(workspaceRoot, modulesRootRelativePath, parsedArchitectureContract);
|
|
2303
|
+
const explicitStandaloneModuleByPath = await buildExplicitStandaloneModulePathMap(workspaceRoot, parsedArchitectureContract);
|
|
2304
|
+
const workspacePackageAliases = await buildWorkspacePackageAliasMap(workspaceRoot, moduleRoots);
|
|
2305
|
+
const imports = options.precomputedImports
|
|
2306
|
+
? [...options.precomputedImports]
|
|
2307
|
+
: await collectCrossModuleImports(workspaceRoot, modulesRootRelativePath, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, adapter);
|
|
2308
|
+
const dependencyContextResolver = parsedArchitectureContract
|
|
2309
|
+
? (0, architectureDependencyContext_1.createArchitectureDependencyContextResolver)(parsedArchitectureContract)
|
|
2310
|
+
: undefined;
|
|
2311
|
+
const peerImports = filterPeerModuleImports(imports);
|
|
2312
|
+
const moduleScopedPeerImports = peerImports.filter((dependencyImport) => isModuleScopedDependencyImport(parsedArchitectureContract, dependencyImport, dependencyContextResolver));
|
|
2313
|
+
const findings = [];
|
|
2314
|
+
const moduleContractCache = new Map();
|
|
2315
|
+
let dependencyRules;
|
|
2316
|
+
let dependencyRulesSkipReason;
|
|
2317
|
+
if (options.dependencyRulesConfigOverride) {
|
|
2318
|
+
dependencyRules = options.dependencyRulesConfigOverride;
|
|
2319
|
+
}
|
|
2320
|
+
else {
|
|
2321
|
+
try {
|
|
2322
|
+
dependencyRules = await readDependencyRulesConfig(workspaceRoot);
|
|
2323
|
+
if (!dependencyRules) {
|
|
2324
|
+
dependencyRulesSkipReason =
|
|
2325
|
+
'.archpilot/dependency-rules.json is missing. Add it to enable config-driven dependency boundary rules.';
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
catch (error) {
|
|
2329
|
+
if (error instanceof DependencyRulesConfigError) {
|
|
2330
|
+
dependencyRulesSkipReason = error.message;
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
throw error;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
if (options.checkCircularDependencies) {
|
|
2338
|
+
const cycles = detectCircularDependencies([...moduleRoots.keys()], moduleScopedPeerImports);
|
|
2339
|
+
if (cycles.length === 0) {
|
|
2340
|
+
findings.push({
|
|
2341
|
+
result: makeValidationResult('AP-DEP-001', 'error', true, `No circular module dependencies detected across ${modulesRootRelativePath}.`),
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
for (const cycle of cycles) {
|
|
2346
|
+
findings.push({
|
|
2347
|
+
result: makeValidationResult('AP-DEP-001', 'error', false, `Circular module dependency detected: ${cycle.replaceAll('->', ' -> ')}.`),
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
if (options.checkForbiddenDependencies) {
|
|
2353
|
+
if (!dependencyRules) {
|
|
2354
|
+
findings.push(...buildDependencyConfigSkippedFindings(dependencyRulesSkipReason ?? 'dependency rules config is unavailable.', {
|
|
2355
|
+
checkForbiddenDependencies: true,
|
|
2356
|
+
checkNonPublicImports: false,
|
|
2357
|
+
}));
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
const violations = moduleScopedPeerImports.filter((dependencyImport) => {
|
|
2361
|
+
if (parsedArchitectureContract) {
|
|
2362
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(parsedArchitectureContract, dependencyImport, dependencyContextResolver);
|
|
2363
|
+
if (dependencyMetadata.isExternal) {
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
const moduleRules = dependencyRules.modules[dependencyImport.sourceModule];
|
|
2368
|
+
if (!moduleRules) {
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
const allowedDependencies = moduleRules.allowedDependencies ?? [];
|
|
2372
|
+
return !allowedDependencies.includes(dependencyImport.targetModule);
|
|
2373
|
+
});
|
|
2374
|
+
if (violations.length === 0) {
|
|
2375
|
+
findings.push({
|
|
2376
|
+
result: makeValidationResult('AP-DEP-002', 'error', true, 'No forbidden cross-module dependencies detected.'),
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
for (const violation of violations) {
|
|
2381
|
+
const dependencyMetadata = parsedArchitectureContract
|
|
2382
|
+
? resolveImportDependencyContextMetadata(parsedArchitectureContract, violation, dependencyContextResolver)
|
|
2383
|
+
: undefined;
|
|
2384
|
+
const allowedDependencies = dependencyRules.modules[violation.sourceModule]?.allowedDependencies ?? [];
|
|
2385
|
+
const allowedDescription = allowedDependencies.length === 0
|
|
2386
|
+
? 'may not depend on any other modules'
|
|
2387
|
+
: `may only depend on: ${allowedDependencies.join(', ')}`;
|
|
2388
|
+
findings.push({
|
|
2389
|
+
result: makeValidationResult('AP-DEP-002', 'error', false, `${violation.sourceFileRelativePath}:${violation.line} imports module "${violation.targetModule}" from "${violation.sourceModule}" via "${violation.importSpecifier}", but "${violation.sourceModule}" ${allowedDescription}.`, {
|
|
2390
|
+
findingType: 'module-dependency',
|
|
2391
|
+
module: violation.sourceModule,
|
|
2392
|
+
target: violation.targetModule,
|
|
2393
|
+
...(dependencyMetadata ?? {}),
|
|
2394
|
+
}),
|
|
2395
|
+
filePath: violation.sourceFileRelativePath,
|
|
2396
|
+
line: violation.line,
|
|
2397
|
+
importSpecifier: violation.importSpecifier,
|
|
2398
|
+
findingType: 'module-dependency',
|
|
2399
|
+
module: violation.sourceModule,
|
|
2400
|
+
target: violation.targetModule,
|
|
2401
|
+
...(dependencyMetadata ?? {}),
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
if (options.checkNonPublicImports) {
|
|
2408
|
+
if (!dependencyRules) {
|
|
2409
|
+
findings.push(...buildDependencyConfigSkippedFindings(dependencyRulesSkipReason ?? 'dependency rules config is unavailable.', {
|
|
2410
|
+
checkForbiddenDependencies: false,
|
|
2411
|
+
checkNonPublicImports: true,
|
|
2412
|
+
}));
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
const violations = [];
|
|
2416
|
+
for (const dependencyImport of moduleScopedPeerImports) {
|
|
2417
|
+
if (parsedArchitectureContract) {
|
|
2418
|
+
const dependencyMetadata = resolveImportDependencyContextMetadata(parsedArchitectureContract, dependencyImport, dependencyContextResolver);
|
|
2419
|
+
if (dependencyMetadata.isExternal) {
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const moduleRules = dependencyRules.modules[dependencyImport.sourceModule];
|
|
2424
|
+
if (moduleRules?.publicEntrypointsOnly !== true || dependencyImport.isPublicImport) {
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (dependencyImport.isPackagePublicImport === true) {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
const targetContract = parsedArchitectureContract
|
|
2431
|
+
? await loadModuleArchitectureContract(workspaceRoot, parsedArchitectureContract, dependencyImport.targetModule, moduleContractCache)
|
|
2432
|
+
: undefined;
|
|
2433
|
+
const publicEntrypoints = await resolveKnownModulePublicEntrypoints(workspaceRoot, parsedArchitectureContract, dependencyImport.targetModule, targetContract?.contract, moduleRoots, adapter);
|
|
2434
|
+
if (publicEntrypoints.length === 0) {
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
if (isImportWithinPublicEntrypoints(dependencyImport.resolvedTargetRelativePath, publicEntrypoints)) {
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
violations.push(dependencyImport);
|
|
2441
|
+
}
|
|
2442
|
+
if (violations.length === 0) {
|
|
2443
|
+
findings.push({
|
|
2444
|
+
result: makeValidationResult('AP-DEP-003', 'error', true, 'No non-public cross-module imports detected.'),
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
else {
|
|
2448
|
+
for (const violation of violations) {
|
|
2449
|
+
const dependencyMetadata = parsedArchitectureContract
|
|
2450
|
+
? resolveImportDependencyContextMetadata(parsedArchitectureContract, violation, dependencyContextResolver)
|
|
2451
|
+
: undefined;
|
|
2452
|
+
const publicImportGuidance = await describeModulePublicImportGuidance(workspaceRoot, violation.targetModule, moduleRoots);
|
|
2453
|
+
findings.push({
|
|
2454
|
+
result: makeValidationResult('AP-DEP-003', 'error', false, `${violation.sourceFileRelativePath}:${violation.line} imports non-public path "${violation.importSpecifier}" from module "${violation.targetModule}". ${publicImportGuidance}`, {
|
|
2455
|
+
findingType: 'module-dependency',
|
|
2456
|
+
module: violation.sourceModule,
|
|
2457
|
+
target: violation.targetModule,
|
|
2458
|
+
...(dependencyMetadata ?? {}),
|
|
2459
|
+
}),
|
|
2460
|
+
filePath: violation.sourceFileRelativePath,
|
|
2461
|
+
line: violation.line,
|
|
2462
|
+
importSpecifier: violation.importSpecifier,
|
|
2463
|
+
findingType: 'module-dependency',
|
|
2464
|
+
module: violation.sourceModule,
|
|
2465
|
+
target: violation.targetModule,
|
|
2466
|
+
...(dependencyMetadata ?? {}),
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (options.checkComponentDependencyPolicies !== false) {
|
|
2473
|
+
findings.push(...validateComponentDependencyPolicies(parsedArchitectureContract, peerImports));
|
|
2474
|
+
}
|
|
2475
|
+
return findings;
|
|
2476
|
+
}
|
|
2477
|
+
async function buildModuleDependencyGraphSnapshot(workspaceRoot, options) {
|
|
2478
|
+
const parsedArchitectureContract = await (0, architectureInit_1.loadArchitectureContract)(workspaceRoot);
|
|
2479
|
+
const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(parsedArchitectureContract);
|
|
2480
|
+
const modulesRootRelativePath = options?.modulesRootRelativePath ??
|
|
2481
|
+
(0, moduleDiscovery_1.resolveModulesRootFromContract)(parsedArchitectureContract);
|
|
2482
|
+
const moduleRoots = await findModuleRoots(workspaceRoot, modulesRootRelativePath, parsedArchitectureContract);
|
|
2483
|
+
const explicitStandaloneModuleByPath = await buildExplicitStandaloneModulePathMap(workspaceRoot, parsedArchitectureContract);
|
|
2484
|
+
const workspacePackageAliases = await buildWorkspacePackageAliasMap(workspaceRoot, moduleRoots);
|
|
2485
|
+
const discoveredModules = sortUnique([
|
|
2486
|
+
...moduleRoots.keys(),
|
|
2487
|
+
...explicitStandaloneModuleByPath.values(),
|
|
2488
|
+
]);
|
|
2489
|
+
const moduleFilterSet = options?.moduleFilter
|
|
2490
|
+
? new Set([...options.moduleFilter].sort((left, right) => left.localeCompare(right)))
|
|
2491
|
+
: undefined;
|
|
2492
|
+
const modules = moduleFilterSet
|
|
2493
|
+
? discoveredModules.filter((moduleId) => moduleFilterSet.has(moduleId))
|
|
2494
|
+
: discoveredModules;
|
|
2495
|
+
const moduleSet = new Set(modules);
|
|
2496
|
+
const allImports = await collectCrossModuleImports(workspaceRoot, modulesRootRelativePath, moduleRoots, explicitStandaloneModuleByPath, workspacePackageAliases, adapter);
|
|
2497
|
+
const imports = filterPeerModuleImports(allImports)
|
|
2498
|
+
.filter((dependencyImport) => moduleSet.has(dependencyImport.sourceModule) &&
|
|
2499
|
+
moduleSet.has(dependencyImport.targetModule))
|
|
2500
|
+
.sort((left, right) => {
|
|
2501
|
+
const sourceCompare = left.sourceModule.localeCompare(right.sourceModule);
|
|
2502
|
+
if (sourceCompare !== 0) {
|
|
2503
|
+
return sourceCompare;
|
|
2504
|
+
}
|
|
2505
|
+
const targetCompare = left.targetModule.localeCompare(right.targetModule);
|
|
2506
|
+
if (targetCompare !== 0) {
|
|
2507
|
+
return targetCompare;
|
|
2508
|
+
}
|
|
2509
|
+
const fileCompare = left.sourceFileRelativePath.localeCompare(right.sourceFileRelativePath);
|
|
2510
|
+
if (fileCompare !== 0) {
|
|
2511
|
+
return fileCompare;
|
|
2512
|
+
}
|
|
2513
|
+
const lineCompare = left.line - right.line;
|
|
2514
|
+
if (lineCompare !== 0) {
|
|
2515
|
+
return lineCompare;
|
|
2516
|
+
}
|
|
2517
|
+
return left.importSpecifier.localeCompare(right.importSpecifier);
|
|
2518
|
+
});
|
|
2519
|
+
const edgeAccumulator = new Map();
|
|
2520
|
+
for (const dependencyImport of imports) {
|
|
2521
|
+
const edgeKey = `${dependencyImport.sourceModule}->${dependencyImport.targetModule}`;
|
|
2522
|
+
const existing = edgeAccumulator.get(edgeKey);
|
|
2523
|
+
if (existing) {
|
|
2524
|
+
existing.importCount += 1;
|
|
2525
|
+
if (dependencyImport.isPublicImport) {
|
|
2526
|
+
existing.publicImportCount += 1;
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
existing.nonPublicImportCount += 1;
|
|
2530
|
+
}
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
edgeAccumulator.set(edgeKey, {
|
|
2534
|
+
sourceModule: dependencyImport.sourceModule,
|
|
2535
|
+
targetModule: dependencyImport.targetModule,
|
|
2536
|
+
importCount: 1,
|
|
2537
|
+
publicImportCount: dependencyImport.isPublicImport ? 1 : 0,
|
|
2538
|
+
nonPublicImportCount: dependencyImport.isPublicImport ? 0 : 1,
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
const edges = [...edgeAccumulator.values()].sort((left, right) => {
|
|
2542
|
+
const sourceCompare = left.sourceModule.localeCompare(right.sourceModule);
|
|
2543
|
+
if (sourceCompare !== 0) {
|
|
2544
|
+
return sourceCompare;
|
|
2545
|
+
}
|
|
2546
|
+
return left.targetModule.localeCompare(right.targetModule);
|
|
2547
|
+
});
|
|
2548
|
+
const cycles = detectCircularDependencies(modules, imports);
|
|
2549
|
+
return {
|
|
2550
|
+
modules,
|
|
2551
|
+
edges,
|
|
2552
|
+
hierarchyEdges: buildModuleHierarchyEdges(modules),
|
|
2553
|
+
cycles,
|
|
2554
|
+
imports,
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
//# sourceMappingURL=dependencyValidation.js.map
|