@dailephd/my-dev-kit-lab 0.3.3 → 0.4.0
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/README.md +46 -269
- package/dist/scripts/security/validate.js +177 -70
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
- package/dist/src/audits/core/collectSourceFacts.js +21 -10
- package/dist/src/mobile/android/audit/androidFinding.js +68 -0
- package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
- package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
- package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
- package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
- package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
- package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
- package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
- package/dist/src/mobile/android/detect/classify.js +150 -0
- package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
- package/dist/src/mobile/android/detect/settingsModules.js +60 -0
- package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
- package/dist/src/mobile/android/detect/traversal.js +79 -0
- package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
- package/dist/src/mobile/android/detection.js +28 -0
- package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
- package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
- package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
- package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
- package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
- package/dist/src/mobile/android/gradle/types.js +1 -0
- package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
- package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
- package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
- package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
- package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
- package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
- package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
- package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
- package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
- package/dist/src/mobile/android/manifest/types.js +29 -0
- package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
- package/dist/src/mobile/android/profile.js +29 -0
- package/dist/src/mobile/android/report/model.js +104 -0
- package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
- package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
- package/dist/src/mobile/android/targetMetadata.js +27 -0
- package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
- package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
- package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
- package/dist/src/mobile/android/validation/checkResult.js +51 -0
- package/dist/src/mobile/android/validation/result.js +25 -0
- package/dist/src/mobile/index.js +42 -0
- package/dist/src/mobile/types.js +27 -0
- package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
- package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
- package/dist/src/securityValidation/validate/cliOptions.js +46 -2
- package/package.json +2 -1
- package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
- package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
- package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
- package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
- package/tests/fixtures/android/java-app/app/build.gradle +16 -0
- package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
- package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
- package/tests/fixtures/android/java-app/build.gradle +3 -0
- package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/java-app/settings.gradle +2 -0
- package/tests/fixtures/android/library/build.gradle.kts +3 -0
- package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
- package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/library/settings.gradle.kts +2 -0
- package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
- package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
- package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
- package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
- package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
- package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
- package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
- package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
- package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
- package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
- package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
- package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
- package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
- package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
- package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
- package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
- package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 2 — deterministic, evidence-based classification policy.
|
|
3
|
+
//
|
|
4
|
+
// Pure functions only (no filesystem access) so the confidence/classification
|
|
5
|
+
// policy can be unit tested independently of traversal. See agents.txt Batch
|
|
6
|
+
// 2 section 7.4 for the evidence-strength tiers this implements:
|
|
7
|
+
// - Strong: an Android plugin id, or namespace/applicationId combined with
|
|
8
|
+
// an Android plugin, or a manifest inside an Android-style source set.
|
|
9
|
+
// - Supporting: source roots, res/, androidTest, wrapper, settings files,
|
|
10
|
+
// catalog aliases, Compose dependencies.
|
|
11
|
+
// - Weak: generic Gradle/Kotlin/Java files or directory names alone.
|
|
12
|
+
// A project must never be confirmed Android from weak evidence alone.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const CONFIDENCE_RANK = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
15
|
+
export function maxConfidence(a, b) {
|
|
16
|
+
return CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b;
|
|
17
|
+
}
|
|
18
|
+
function capConfidence(confidence, cap) {
|
|
19
|
+
return CONFIDENCE_RANK[confidence] <= CONFIDENCE_RANK[cap] ? confidence : cap;
|
|
20
|
+
}
|
|
21
|
+
// Classifies a single Gradle module from its collected static evidence.
|
|
22
|
+
// Directory naming (e.g. a module literally called "app") is never used as a
|
|
23
|
+
// signal — only plugin declarations, manifest presence, and Android-specific
|
|
24
|
+
// identifiers.
|
|
25
|
+
export function classifyModule(input) {
|
|
26
|
+
const evidence = [];
|
|
27
|
+
if (input.hasAndroidApplicationPlugin) {
|
|
28
|
+
evidence.push("com.android.application plugin declared");
|
|
29
|
+
const strongCombo = input.hasManifest || Boolean(input.namespace) || Boolean(input.applicationId);
|
|
30
|
+
if (input.hasManifest)
|
|
31
|
+
evidence.push("AndroidManifest.xml present in module source set");
|
|
32
|
+
if (input.namespace)
|
|
33
|
+
evidence.push(`namespace = "${input.namespace}"`);
|
|
34
|
+
if (input.applicationId)
|
|
35
|
+
evidence.push(`applicationId = "${input.applicationId}"`);
|
|
36
|
+
return { kind: "application", confidence: strongCombo ? "high" : "medium", evidence };
|
|
37
|
+
}
|
|
38
|
+
if (input.hasAndroidLibraryPlugin) {
|
|
39
|
+
evidence.push("com.android.library plugin declared");
|
|
40
|
+
const strongCombo = input.hasManifest || Boolean(input.namespace);
|
|
41
|
+
if (input.hasManifest)
|
|
42
|
+
evidence.push("AndroidManifest.xml present in module source set");
|
|
43
|
+
if (input.namespace)
|
|
44
|
+
evidence.push(`namespace = "${input.namespace}"`);
|
|
45
|
+
return { kind: "library", confidence: strongCombo ? "high" : "medium", evidence };
|
|
46
|
+
}
|
|
47
|
+
if (input.hasCatalogAndroidPluginAliasEvidence) {
|
|
48
|
+
evidence.push("version-catalog plugin alias referencing an Android Gradle Plugin entry");
|
|
49
|
+
return { kind: "unknown", confidence: "medium", evidence };
|
|
50
|
+
}
|
|
51
|
+
// Generic Java/Kotlin source roots are deliberately excluded from this
|
|
52
|
+
// list: they exist in any JVM project and are not Android-specific, so
|
|
53
|
+
// they must never by themselves push a module toward Android detection
|
|
54
|
+
// (agents.txt Batch 2 section 12.4 — generic Gradle/Kotlin/Java evidence
|
|
55
|
+
// must not trigger Android detection without Android-specific evidence).
|
|
56
|
+
const weakSignals = [];
|
|
57
|
+
if (input.hasManifest)
|
|
58
|
+
weakSignals.push("AndroidManifest.xml present without a resolved Android plugin");
|
|
59
|
+
if (input.namespace)
|
|
60
|
+
weakSignals.push(`namespace = "${input.namespace}" without a resolved Android plugin`);
|
|
61
|
+
if (input.applicationId)
|
|
62
|
+
weakSignals.push(`applicationId = "${input.applicationId}" without a resolved Android plugin`);
|
|
63
|
+
if (input.hasResDir)
|
|
64
|
+
weakSignals.push("res/ directory present");
|
|
65
|
+
if (weakSignals.length > 0) {
|
|
66
|
+
evidence.push(...weakSignals);
|
|
67
|
+
return { kind: "unknown", confidence: "low", evidence };
|
|
68
|
+
}
|
|
69
|
+
if (input.hasGenericBuildFile) {
|
|
70
|
+
evidence.push("Gradle build file present with no Android-specific evidence");
|
|
71
|
+
return { kind: "non-android", confidence: "medium", evidence };
|
|
72
|
+
}
|
|
73
|
+
return { kind: "unknown", confidence: "unknown", evidence: [] };
|
|
74
|
+
}
|
|
75
|
+
// Classifies UI toolkit usage for a single module. Conservative by design:
|
|
76
|
+
// Kotlin usage alone never implies Compose, and res/ alone never implies
|
|
77
|
+
// XML/View classification beyond what's asserted here (a literal layout
|
|
78
|
+
// resource file, not merely a res/ directory).
|
|
79
|
+
export function classifyModuleUiToolkit(input) {
|
|
80
|
+
const evidence = [];
|
|
81
|
+
const strongCompose = input.composeBuildFeatureEvidence || input.composeDependencyEvidence || input.composeSourceEvidence;
|
|
82
|
+
const strongView = input.hasResLayout;
|
|
83
|
+
if (input.composeBuildFeatureEvidence)
|
|
84
|
+
evidence.push("buildFeatures { compose = true }");
|
|
85
|
+
if (input.composeDependencyEvidence)
|
|
86
|
+
evidence.push("Compose dependency evidence in build file");
|
|
87
|
+
if (input.composeSourceEvidence)
|
|
88
|
+
evidence.push("@Composable/androidx.compose import evidence in source");
|
|
89
|
+
if (input.composePluginEvidence)
|
|
90
|
+
evidence.push("Compose compiler plugin evidence");
|
|
91
|
+
if (input.hasResLayout)
|
|
92
|
+
evidence.push("res/layout XML resource present");
|
|
93
|
+
if (input.viewSourceEvidence)
|
|
94
|
+
evidence.push("traditional Android View usage evidence in source");
|
|
95
|
+
if (strongCompose && strongView) {
|
|
96
|
+
return { uiToolkit: "mixed", evidence };
|
|
97
|
+
}
|
|
98
|
+
if (strongCompose) {
|
|
99
|
+
return { uiToolkit: "compose", evidence };
|
|
100
|
+
}
|
|
101
|
+
if (strongView) {
|
|
102
|
+
return { uiToolkit: "xml-view", evidence };
|
|
103
|
+
}
|
|
104
|
+
return { uiToolkit: "uncertain", evidence };
|
|
105
|
+
}
|
|
106
|
+
// Combines per-module kinds into the overall project-level classification.
|
|
107
|
+
// Uses the widened ANDROID_PROJECT_KINDS vocabulary (see detection.ts) — a
|
|
108
|
+
// project with both application and library modules is "mixed"; more than
|
|
109
|
+
// one Android module of the same kind is "multi-module"; a single confirmed
|
|
110
|
+
// module keeps its own kind; Android-ish-but-unresolved evidence with no
|
|
111
|
+
// confirmed module is "partial"; nothing at all is "non-android".
|
|
112
|
+
export function combineProjectKind(modules, hasUnresolvedAndroidEvidence) {
|
|
113
|
+
const applicationCount = modules.filter((module) => module.kind === "application").length;
|
|
114
|
+
const libraryCount = modules.filter((module) => module.kind === "library").length;
|
|
115
|
+
const unknownCount = modules.filter((module) => module.kind === "unknown").length;
|
|
116
|
+
if (applicationCount >= 1 && libraryCount >= 1)
|
|
117
|
+
return "mixed";
|
|
118
|
+
if (applicationCount + libraryCount > 1)
|
|
119
|
+
return "multi-module";
|
|
120
|
+
if (applicationCount === 1)
|
|
121
|
+
return "application";
|
|
122
|
+
if (libraryCount === 1)
|
|
123
|
+
return "library";
|
|
124
|
+
if (unknownCount > 0 || hasUnresolvedAndroidEvidence)
|
|
125
|
+
return "partial";
|
|
126
|
+
return "non-android";
|
|
127
|
+
}
|
|
128
|
+
export function combineProjectUiToolkit(modules) {
|
|
129
|
+
const androidModules = modules.filter((module) => module.kind === "application" || module.kind === "library");
|
|
130
|
+
const composeCount = androidModules.filter((module) => module.uiToolkit === "compose").length;
|
|
131
|
+
const viewCount = androidModules.filter((module) => module.uiToolkit === "xml-view").length;
|
|
132
|
+
const mixedCount = androidModules.filter((module) => module.uiToolkit === "mixed").length;
|
|
133
|
+
if (mixedCount > 0 || (composeCount > 0 && viewCount > 0))
|
|
134
|
+
return "mixed";
|
|
135
|
+
if (composeCount > 0)
|
|
136
|
+
return "compose";
|
|
137
|
+
if (viewCount > 0)
|
|
138
|
+
return "xml-view";
|
|
139
|
+
return "uncertain";
|
|
140
|
+
}
|
|
141
|
+
export function combineProjectConfidence(projectKind, moduleConfidences, hasNonAndroidEvidenceOnly) {
|
|
142
|
+
if (projectKind === "non-android") {
|
|
143
|
+
return hasNonAndroidEvidenceOnly ? "high" : "unknown";
|
|
144
|
+
}
|
|
145
|
+
const strongest = moduleConfidences.reduce((acc, confidence) => maxConfidence(acc, confidence), "unknown");
|
|
146
|
+
if (projectKind === "partial") {
|
|
147
|
+
return capConfidence(strongest, "medium");
|
|
148
|
+
}
|
|
149
|
+
return strongest;
|
|
150
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { extractBuildFileEvidence } from "./buildFileEvidence.js";
|
|
4
|
+
import { parseDeclaredModules } from "./settingsModules.js";
|
|
5
|
+
import { extractVersionCatalogEvidence } from "./versionCatalogEvidence.js";
|
|
6
|
+
import { scanSourceRootsForUiEvidence } from "./sourceEvidence.js";
|
|
7
|
+
import { walkAndroidCandidateFiles } from "./traversal.js";
|
|
8
|
+
import { classifyModule, classifyModuleUiToolkit, combineProjectConfidence, combineProjectKind, combineProjectUiToolkit, } from "./classify.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// v0.4.0 Batch 2 — Android project detector entry point.
|
|
11
|
+
//
|
|
12
|
+
// Static, deterministic, read-only, network-free. Does not invoke Gradle, the
|
|
13
|
+
// Android SDK, or a Java toolchain. Populates the Batch 1
|
|
14
|
+
// AndroidDetectionResult contract only — manifest content parsing, full
|
|
15
|
+
// Gradle metadata, and verdict calculation are later batches' scope.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function dirOf(relativeFilePath) {
|
|
18
|
+
const dir = path.posix.dirname(relativeFilePath.replace(/\\/g, "/"));
|
|
19
|
+
return dir === "." ? "" : dir;
|
|
20
|
+
}
|
|
21
|
+
function baseNameOf(relativeFilePath) {
|
|
22
|
+
return path.posix.basename(relativeFilePath.replace(/\\/g, "/"));
|
|
23
|
+
}
|
|
24
|
+
function joinRelative(dir, name) {
|
|
25
|
+
return dir === "" ? name : `${dir}/${name}`;
|
|
26
|
+
}
|
|
27
|
+
function catalogAliasToAccessorSuffix(alias) {
|
|
28
|
+
return alias.replace(/-/g, ".");
|
|
29
|
+
}
|
|
30
|
+
function readTextSafe(absolutePath) {
|
|
31
|
+
try {
|
|
32
|
+
return readFileSync(absolutePath, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function detectAndroidProject(targetRoot) {
|
|
39
|
+
const resolvedRoot = path.resolve(targetRoot);
|
|
40
|
+
const { relativeFilePaths, skippedSymlinks } = walkAndroidCandidateFiles(resolvedRoot);
|
|
41
|
+
const fileSet = new Set(relativeFilePaths);
|
|
42
|
+
const warnings = [];
|
|
43
|
+
for (const skipped of skippedSymlinks) {
|
|
44
|
+
warnings.push(`Skipped directory symlink (not followed): ${skipped}`);
|
|
45
|
+
}
|
|
46
|
+
// ---- Root-level evidence -------------------------------------------------
|
|
47
|
+
const rootBuildFiles = relativeFilePaths
|
|
48
|
+
.filter((p) => dirOf(p) === "" && (baseNameOf(p) === "build.gradle" || baseNameOf(p) === "build.gradle.kts"))
|
|
49
|
+
.sort((a, b) => a.localeCompare(b));
|
|
50
|
+
const gradleSettingsFiles = relativeFilePaths
|
|
51
|
+
.filter((p) => dirOf(p) === "" && (baseNameOf(p) === "settings.gradle" || baseNameOf(p) === "settings.gradle.kts"))
|
|
52
|
+
.sort((a, b) => a.localeCompare(b));
|
|
53
|
+
if (gradleSettingsFiles.length > 1) {
|
|
54
|
+
warnings.push(`Multiple root settings files found (${gradleSettingsFiles.join(", ")}); using "${gradleSettingsFiles[0]}" as the primary evidence source.`);
|
|
55
|
+
}
|
|
56
|
+
const hasGradleWrapper = fileSet.has("gradlew") || fileSet.has("gradlew.bat") || fileSet.has("gradle/wrapper/gradle-wrapper.properties");
|
|
57
|
+
const versionCatalogFiles = relativeFilePaths.filter((p) => p === "gradle/libs.versions.toml");
|
|
58
|
+
const catalogEvidence = versionCatalogFiles.length > 0
|
|
59
|
+
? extractVersionCatalogEvidence(readTextSafe(path.join(resolvedRoot, versionCatalogFiles[0])) ?? "")
|
|
60
|
+
: { androidPluginAliasEvidence: [], kotlinAndroidPluginAliasEvidence: [], composeAliasEvidence: [] };
|
|
61
|
+
// ---- Module discovery ------------------------------------------------------
|
|
62
|
+
const structuralModuleDirs = new Set();
|
|
63
|
+
for (const p of relativeFilePaths) {
|
|
64
|
+
const name = baseNameOf(p);
|
|
65
|
+
if (name === "build.gradle" || name === "build.gradle.kts") {
|
|
66
|
+
const dir = dirOf(p);
|
|
67
|
+
if (dir !== "")
|
|
68
|
+
structuralModuleDirs.add(dir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let declaredModulePaths = [];
|
|
72
|
+
let unsupportedDeclarations = [];
|
|
73
|
+
if (gradleSettingsFiles.length > 0) {
|
|
74
|
+
const settingsText = readTextSafe(path.join(resolvedRoot, gradleSettingsFiles[0])) ?? "";
|
|
75
|
+
const parsed = parseDeclaredModules(settingsText);
|
|
76
|
+
declaredModulePaths = parsed.declaredModulePaths;
|
|
77
|
+
unsupportedDeclarations = parsed.unsupportedDeclarations;
|
|
78
|
+
}
|
|
79
|
+
for (const declaration of unsupportedDeclarations) {
|
|
80
|
+
warnings.push(`Unsupported or dynamic module declaration in settings file (preserved, not evaluated): ${declaration}`);
|
|
81
|
+
}
|
|
82
|
+
const declaredOnlyDirs = declaredModulePaths.filter((dir) => !structuralModuleDirs.has(dir));
|
|
83
|
+
for (const dir of declaredOnlyDirs) {
|
|
84
|
+
warnings.push(`Module "${dir}" is declared in the settings file but no build.gradle(.kts) was found for it.`);
|
|
85
|
+
}
|
|
86
|
+
const allModuleDirs = [...new Set([...structuralModuleDirs, ...declaredModulePaths])].sort((a, b) => a.localeCompare(b));
|
|
87
|
+
// ---- Per-module evidence and classification --------------------------------
|
|
88
|
+
const modules = [];
|
|
89
|
+
const javaSourceRoots = [];
|
|
90
|
+
const kotlinSourceRoots = [];
|
|
91
|
+
const unitTestSourceRoots = [];
|
|
92
|
+
const instrumentedTestSourceRoots = [];
|
|
93
|
+
const manifestPaths = [];
|
|
94
|
+
let androidGradlePluginEvidence;
|
|
95
|
+
let kotlinAndroidPluginEvidence;
|
|
96
|
+
let namespaceEvidence;
|
|
97
|
+
let applicationIdEvidence;
|
|
98
|
+
for (const moduleDir of allModuleDirs) {
|
|
99
|
+
const buildFileRelPath = [`${moduleDir}/build.gradle.kts`, `${moduleDir}/build.gradle`].find((p) => fileSet.has(p));
|
|
100
|
+
const buildFileText = buildFileRelPath ? readTextSafe(path.join(resolvedRoot, buildFileRelPath)) ?? "" : undefined;
|
|
101
|
+
const buildEvidence = buildFileText !== undefined ? extractBuildFileEvidence(buildFileText) : undefined;
|
|
102
|
+
const hasCatalogAliasEvidence = buildFileText !== undefined &&
|
|
103
|
+
[...catalogEvidence.androidPluginAliasEvidence, ...catalogEvidence.kotlinAndroidPluginAliasEvidence].some((alias) => buildFileText.includes(`libs.plugins.${catalogAliasToAccessorSuffix(alias)}`));
|
|
104
|
+
const manifestRelPath = `${moduleDir}/src/main/AndroidManifest.xml`;
|
|
105
|
+
const hasManifest = fileSet.has(manifestRelPath);
|
|
106
|
+
const moduleManifestPaths = hasManifest ? [manifestRelPath] : [];
|
|
107
|
+
const javaRootPrefix = `${moduleDir}/src/main/java/`;
|
|
108
|
+
const kotlinRootPrefix = `${moduleDir}/src/main/kotlin/`;
|
|
109
|
+
const unitTestPrefix = `${moduleDir}/src/test/`;
|
|
110
|
+
const instrumentedTestPrefix = `${moduleDir}/src/androidTest/`;
|
|
111
|
+
const resLayoutPrefix = `${moduleDir}/src/main/res/layout`;
|
|
112
|
+
// Language is classified by file extension found under each directory,
|
|
113
|
+
// not by directory name alone — Kotlin sources are conventionally placed
|
|
114
|
+
// under src/main/java in some older project layouts, so a "java"-named
|
|
115
|
+
// directory containing only .kt files is a Kotlin source root, not a
|
|
116
|
+
// Java one (agents.txt Batch 2 section 7.9).
|
|
117
|
+
const javaRootHasJavaFiles = relativeFilePaths.some((p) => p.startsWith(javaRootPrefix) && p.endsWith(".java"));
|
|
118
|
+
const javaRootHasKotlinFiles = relativeFilePaths.some((p) => p.startsWith(javaRootPrefix) && p.endsWith(".kt"));
|
|
119
|
+
const kotlinRootHasKotlinFiles = relativeFilePaths.some((p) => p.startsWith(kotlinRootPrefix) && p.endsWith(".kt"));
|
|
120
|
+
const kotlinRootHasJavaFiles = relativeFilePaths.some((p) => p.startsWith(kotlinRootPrefix) && p.endsWith(".java"));
|
|
121
|
+
const hasJavaRoot = javaRootHasJavaFiles || kotlinRootHasJavaFiles;
|
|
122
|
+
const hasKotlinRoot = javaRootHasKotlinFiles || kotlinRootHasKotlinFiles;
|
|
123
|
+
const hasUnitTestRoot = relativeFilePaths.some((p) => p.startsWith(unitTestPrefix));
|
|
124
|
+
const hasInstrumentedTestRoot = relativeFilePaths.some((p) => p.startsWith(instrumentedTestPrefix));
|
|
125
|
+
const hasResLayout = relativeFilePaths.some((p) => p.startsWith(resLayoutPrefix) && p.endsWith(".xml"));
|
|
126
|
+
if (javaRootHasJavaFiles)
|
|
127
|
+
javaSourceRoots.push(`${moduleDir}/src/main/java`);
|
|
128
|
+
if (kotlinRootHasJavaFiles)
|
|
129
|
+
javaSourceRoots.push(`${moduleDir}/src/main/kotlin`);
|
|
130
|
+
if (kotlinRootHasKotlinFiles)
|
|
131
|
+
kotlinSourceRoots.push(`${moduleDir}/src/main/kotlin`);
|
|
132
|
+
if (javaRootHasKotlinFiles)
|
|
133
|
+
kotlinSourceRoots.push(`${moduleDir}/src/main/java`);
|
|
134
|
+
if (hasUnitTestRoot)
|
|
135
|
+
unitTestSourceRoots.push(`${moduleDir}/src/test`);
|
|
136
|
+
if (hasInstrumentedTestRoot)
|
|
137
|
+
instrumentedTestSourceRoots.push(`${moduleDir}/src/androidTest`);
|
|
138
|
+
manifestPaths.push(...moduleManifestPaths);
|
|
139
|
+
const sourceUiEvidence = scanSourceRootsForUiEvidence([javaRootHasJavaFiles || javaRootHasKotlinFiles ? path.join(resolvedRoot, moduleDir, "src", "main", "java") : undefined,
|
|
140
|
+
kotlinRootHasKotlinFiles || kotlinRootHasJavaFiles ? path.join(resolvedRoot, moduleDir, "src", "main", "kotlin") : undefined]
|
|
141
|
+
.filter((p) => Boolean(p)));
|
|
142
|
+
const classification = classifyModule({
|
|
143
|
+
hasAndroidApplicationPlugin: buildEvidence?.androidApplicationPlugin ?? false,
|
|
144
|
+
hasAndroidLibraryPlugin: buildEvidence?.androidLibraryPlugin ?? false,
|
|
145
|
+
hasCatalogAndroidPluginAliasEvidence: hasCatalogAliasEvidence,
|
|
146
|
+
namespace: buildEvidence?.namespace,
|
|
147
|
+
applicationId: buildEvidence?.applicationId,
|
|
148
|
+
hasManifest,
|
|
149
|
+
hasSrcRoots: hasJavaRoot || hasKotlinRoot,
|
|
150
|
+
hasResDir: relativeFilePaths.some((p) => p.startsWith(`${moduleDir}/src/main/res/`)),
|
|
151
|
+
hasGenericBuildFile: buildFileText !== undefined,
|
|
152
|
+
});
|
|
153
|
+
const uiClassification = classifyModuleUiToolkit({
|
|
154
|
+
composeBuildFeatureEvidence: buildEvidence?.composeBuildFeatureEvidence ?? false,
|
|
155
|
+
composeDependencyEvidence: buildEvidence?.composeDependencyEvidence ?? false,
|
|
156
|
+
composePluginEvidence: buildEvidence?.composePluginEvidence ?? false,
|
|
157
|
+
composeSourceEvidence: sourceUiEvidence.composeSourceEvidence,
|
|
158
|
+
hasResLayout,
|
|
159
|
+
viewSourceEvidence: sourceUiEvidence.viewSourceEvidence,
|
|
160
|
+
});
|
|
161
|
+
if (buildEvidence?.androidApplicationPlugin && !androidGradlePluginEvidence) {
|
|
162
|
+
androidGradlePluginEvidence = `com.android.application (${buildFileRelPath})`;
|
|
163
|
+
}
|
|
164
|
+
if (buildEvidence?.androidLibraryPlugin && !androidGradlePluginEvidence) {
|
|
165
|
+
androidGradlePluginEvidence = `com.android.library (${buildFileRelPath})`;
|
|
166
|
+
}
|
|
167
|
+
if (buildEvidence?.kotlinAndroidPlugin && !kotlinAndroidPluginEvidence) {
|
|
168
|
+
kotlinAndroidPluginEvidence = `org.jetbrains.kotlin.android (${buildFileRelPath})`;
|
|
169
|
+
}
|
|
170
|
+
if (buildEvidence?.namespace && !namespaceEvidence) {
|
|
171
|
+
namespaceEvidence = buildEvidence.namespace;
|
|
172
|
+
}
|
|
173
|
+
if (buildEvidence?.applicationId && !applicationIdEvidence) {
|
|
174
|
+
applicationIdEvidence = buildEvidence.applicationId;
|
|
175
|
+
}
|
|
176
|
+
modules.push({
|
|
177
|
+
path: moduleDir,
|
|
178
|
+
kind: classification.kind,
|
|
179
|
+
manifestPaths: moduleManifestPaths,
|
|
180
|
+
namespaceEvidence: buildEvidence?.namespace,
|
|
181
|
+
applicationIdEvidence: buildEvidence?.applicationId,
|
|
182
|
+
confidence: classification.confidence,
|
|
183
|
+
evidence: classification.evidence,
|
|
184
|
+
uiToolkit: uiClassification.uiToolkit,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ---- Project-level aggregation ---------------------------------------------
|
|
188
|
+
const applicationModules = modules.filter((m) => m.kind === "application").map((m) => m.path).sort((a, b) => a.localeCompare(b));
|
|
189
|
+
const libraryModules = modules.filter((m) => m.kind === "library").map((m) => m.path).sort((a, b) => a.localeCompare(b));
|
|
190
|
+
const hasUnresolvedAndroidEvidence = declaredOnlyDirs.length > 0 || (rootBuildFiles.length > 0 && modules.some((m) => m.kind === "unknown"));
|
|
191
|
+
let projectKind = combineProjectKind(modules.map((m) => ({ kind: m.kind, uiToolkit: m.uiToolkit ?? "uncertain" })), hasUnresolvedAndroidEvidence);
|
|
192
|
+
// A single confirmed application/library module whose manifest is entirely
|
|
193
|
+
// missing is Android-specific evidence with an incomplete structure, not a
|
|
194
|
+
// confidently confirmed Android project — downgrade to "partial" rather
|
|
195
|
+
// than reporting a false-confident "application"/"library" classification
|
|
196
|
+
// (agents.txt Batch 2 section 12.3). Multi-module/mixed projects where only
|
|
197
|
+
// some modules lack a manifest keep their combined kind; the missing
|
|
198
|
+
// manifest is still surfaced via partialOrUnsupportedStructure below.
|
|
199
|
+
const androidModules = modules.filter((m) => m.kind === "application" || m.kind === "library");
|
|
200
|
+
const allAndroidModulesMissingManifest = androidModules.length > 0 && androidModules.every((m) => m.manifestPaths.length === 0);
|
|
201
|
+
if (allAndroidModulesMissingManifest && (projectKind === "application" || projectKind === "library")) {
|
|
202
|
+
projectKind = "partial";
|
|
203
|
+
}
|
|
204
|
+
const uiToolkit = combineProjectUiToolkit(modules.map((m) => ({ kind: m.kind, uiToolkit: m.uiToolkit ?? "uncertain" })));
|
|
205
|
+
const hasNonAndroidEvidenceOnly = rootBuildFiles.length > 0 || modules.some((m) => m.kind === "non-android");
|
|
206
|
+
const confidence = combineProjectConfidence(projectKind, modules.filter((m) => m.kind === "application" || m.kind === "library" || m.kind === "unknown").map((m) => m.confidence ?? "unknown"), hasNonAndroidEvidenceOnly);
|
|
207
|
+
const androidModulesMissingManifest = modules.filter((m) => (m.kind === "application" || m.kind === "library") && m.manifestPaths.length === 0);
|
|
208
|
+
for (const module of androidModulesMissingManifest) {
|
|
209
|
+
warnings.push(`No AndroidManifest.xml found for Android module "${module.path}".`);
|
|
210
|
+
}
|
|
211
|
+
const partialOrUnsupportedStructure = projectKind === "partial" || androidModulesMissingManifest.length > 0 || unsupportedDeclarations.length > 0 || declaredOnlyDirs.length > 0;
|
|
212
|
+
const evidence = [];
|
|
213
|
+
if (applicationModules.length > 0)
|
|
214
|
+
evidence.push(`${applicationModules.length} application module(s) found: ${applicationModules.join(", ")}`);
|
|
215
|
+
if (libraryModules.length > 0)
|
|
216
|
+
evidence.push(`${libraryModules.length} library module(s) found: ${libraryModules.join(", ")}`);
|
|
217
|
+
if (hasGradleWrapper)
|
|
218
|
+
evidence.push("Gradle wrapper present");
|
|
219
|
+
if (gradleSettingsFiles.length > 0)
|
|
220
|
+
evidence.push(`Gradle settings file(s): ${gradleSettingsFiles.join(", ")}`);
|
|
221
|
+
if (catalogEvidence.androidPluginAliasEvidence.length > 0) {
|
|
222
|
+
evidence.push(`Version-catalog Android plugin alias(es): ${catalogEvidence.androidPluginAliasEvidence.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
if (catalogEvidence.composeAliasEvidence.length > 0) {
|
|
225
|
+
evidence.push(`Version-catalog Compose alias(es): ${catalogEvidence.composeAliasEvidence.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
detected: projectKind !== "non-android",
|
|
229
|
+
confidence,
|
|
230
|
+
evidence,
|
|
231
|
+
projectKind,
|
|
232
|
+
uiToolkit,
|
|
233
|
+
hasGradleWrapper,
|
|
234
|
+
gradleSettingsFiles,
|
|
235
|
+
rootBuildFiles,
|
|
236
|
+
versionCatalogFiles,
|
|
237
|
+
modules,
|
|
238
|
+
applicationModules,
|
|
239
|
+
libraryModules,
|
|
240
|
+
manifestPaths: [...new Set(manifestPaths)].sort((a, b) => a.localeCompare(b)),
|
|
241
|
+
javaSourceRoots: [...new Set(javaSourceRoots)].sort((a, b) => a.localeCompare(b)),
|
|
242
|
+
kotlinSourceRoots: [...new Set(kotlinSourceRoots)].sort((a, b) => a.localeCompare(b)),
|
|
243
|
+
unitTestSourceRoots: [...new Set(unitTestSourceRoots)].sort((a, b) => a.localeCompare(b)),
|
|
244
|
+
instrumentedTestSourceRoots: [...new Set(instrumentedTestSourceRoots)].sort((a, b) => a.localeCompare(b)),
|
|
245
|
+
androidGradlePluginEvidence,
|
|
246
|
+
kotlinAndroidPluginEvidence,
|
|
247
|
+
namespaceEvidence,
|
|
248
|
+
applicationIdEvidence,
|
|
249
|
+
partialOrUnsupportedStructure,
|
|
250
|
+
warnings,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 2 — bounded, deterministic settings.gradle(.kts) module parser.
|
|
3
|
+
//
|
|
4
|
+
// Extracts module declarations from common static `include(...)` syntax
|
|
5
|
+
// (Groovy and Kotlin DSL) using lexical regex scanning. This intentionally
|
|
6
|
+
// does not evaluate Gradle: no variables, functions, conditionals, loops, or
|
|
7
|
+
// included builds are resolved. Anything that cannot be statically resolved
|
|
8
|
+
// is reported as an unsupported declaration rather than guessed.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Matches `include` (Groovy bare-args or Kotlin-DSL call) but not
|
|
11
|
+
// `includeBuild` (composite builds are out of scope for this batch).
|
|
12
|
+
const INCLUDE_CALL_PATTERN = /\binclude\b(?!Build)\s*(\(([\s\S]*?)\)|([^\n;]*))/g;
|
|
13
|
+
const QUOTED_STRING_PATTERN = /['"]([^'"]*)['"]/g;
|
|
14
|
+
const DYNAMIC_EXPRESSION_PATTERN = /\$\{|\$\w|\bfor\s*\(|\bif\s*\(|\bwhile\s*\(|[+]{1}\s*['"$]/;
|
|
15
|
+
function gradlePathToRelativeDir(gradlePath) {
|
|
16
|
+
if (!gradlePath.startsWith(":")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const segments = gradlePath.split(":").filter((segment) => segment.length > 0);
|
|
20
|
+
if (segments.length === 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return segments.join("/");
|
|
24
|
+
}
|
|
25
|
+
// Parses declared module paths out of a settings.gradle or settings.gradle.kts
|
|
26
|
+
// file's raw text. Supports common Groovy and Kotlin DSL `include(...)` forms
|
|
27
|
+
// (single or multiple arguments, single-line or simple multiline calls).
|
|
28
|
+
// Dynamic/unresolvable declarations are preserved verbatim (trimmed, bounded)
|
|
29
|
+
// in `unsupportedDeclarations` rather than silently dropped.
|
|
30
|
+
export function parseDeclaredModules(settingsText) {
|
|
31
|
+
const declaredModulePaths = new Set();
|
|
32
|
+
const unsupportedDeclarations = [];
|
|
33
|
+
for (const match of settingsText.matchAll(INCLUDE_CALL_PATTERN)) {
|
|
34
|
+
const body = match[2] ?? match[3] ?? "";
|
|
35
|
+
let addedAny = false;
|
|
36
|
+
let dynamicFound = DYNAMIC_EXPRESSION_PATTERN.test(body);
|
|
37
|
+
for (const quoted of body.matchAll(QUOTED_STRING_PATTERN)) {
|
|
38
|
+
const literal = quoted[1];
|
|
39
|
+
// A quoted literal containing an unresolved `$` interpolation is not a
|
|
40
|
+
// static module path, even though it matched the quoted-string regex —
|
|
41
|
+
// it must be reported as unsupported, not silently accepted.
|
|
42
|
+
if (literal.includes("$")) {
|
|
43
|
+
dynamicFound = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const relativeDir = gradlePathToRelativeDir(literal);
|
|
47
|
+
if (relativeDir) {
|
|
48
|
+
declaredModulePaths.add(relativeDir);
|
|
49
|
+
addedAny = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (dynamicFound || (!addedAny && body.trim().length > 0)) {
|
|
53
|
+
unsupportedDeclarations.push(body.trim().slice(0, 200));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
declaredModulePaths: [...declaredModulePaths].sort((a, b) => a.localeCompare(b)),
|
|
58
|
+
unsupportedDeclarations,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 2 — bounded source inspection for Compose/View UI evidence.
|
|
5
|
+
//
|
|
6
|
+
// Scans a small, capped number of source files under given source roots for
|
|
7
|
+
// lexical markers (@Composable, androidx.compose imports, traditional View
|
|
8
|
+
// APIs). This is not semantic parsing — it is a bounded grep-style scan with
|
|
9
|
+
// hard file-count and file-size caps so detection stays fast and safe on
|
|
10
|
+
// large or adversarial repositories.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const MAX_FILES_SCANNED = 40;
|
|
13
|
+
const MAX_FILE_SIZE_BYTES = 200_000;
|
|
14
|
+
const COMPOSE_SOURCE_PATTERN = /@Composable|androidx\.compose/;
|
|
15
|
+
const VIEW_SOURCE_PATTERN = /\bandroid\.view\.View\b|\bandroid\.widget\.|findViewById|\bViewBinding\b/;
|
|
16
|
+
function collectSourceFiles(root, extensions, budget) {
|
|
17
|
+
const files = [];
|
|
18
|
+
const visit = (dir) => {
|
|
19
|
+
if (budget.remaining <= 0)
|
|
20
|
+
return;
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (budget.remaining <= 0)
|
|
30
|
+
return;
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
visit(fullPath);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
budget.remaining -= 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
visit(root);
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
// Scans up to MAX_FILES_SCANNED Kotlin/Java files across the given source
|
|
46
|
+
// roots (already resolved absolute directories) for bounded Compose/View
|
|
47
|
+
// lexical evidence. Missing directories are ignored, not treated as errors.
|
|
48
|
+
export function scanSourceRootsForUiEvidence(sourceRootAbsolutePaths) {
|
|
49
|
+
const budget = { remaining: MAX_FILES_SCANNED };
|
|
50
|
+
const files = [];
|
|
51
|
+
for (const root of sourceRootAbsolutePaths) {
|
|
52
|
+
if (budget.remaining <= 0)
|
|
53
|
+
break;
|
|
54
|
+
try {
|
|
55
|
+
if (!statSync(root).isDirectory())
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
files.push(...collectSourceFiles(root, [".kt", ".java"], budget));
|
|
62
|
+
}
|
|
63
|
+
let composeSourceEvidence = false;
|
|
64
|
+
let viewSourceEvidence = false;
|
|
65
|
+
let filesScanned = 0;
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
let stat;
|
|
68
|
+
try {
|
|
69
|
+
stat = statSync(file);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (stat.size > MAX_FILE_SIZE_BYTES)
|
|
75
|
+
continue;
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
content = readFileSync(file, "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
filesScanned += 1;
|
|
84
|
+
if (!composeSourceEvidence && COMPOSE_SOURCE_PATTERN.test(content)) {
|
|
85
|
+
composeSourceEvidence = true;
|
|
86
|
+
}
|
|
87
|
+
if (!viewSourceEvidence && VIEW_SOURCE_PATTERN.test(content)) {
|
|
88
|
+
viewSourceEvidence = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { composeSourceEvidence, viewSourceEvidence, filesScanned };
|
|
92
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { relativeWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.0 Batch 2 — bounded, deterministic filesystem traversal for Android
|
|
6
|
+
// detection.
|
|
7
|
+
//
|
|
8
|
+
// A dedicated walker (rather than reusing src/core/fileGlobs.ts) because
|
|
9
|
+
// Android detection needs a wider and different exclusion set (.gradle, out,
|
|
10
|
+
// reports, lab-output, .my-dev-kit, IDE caches) than the generic glob helper
|
|
11
|
+
// currently declares, and widening that shared helper's exclusion list would
|
|
12
|
+
// change behavior for every other consumer of collectFilesForGlobs. Reuses
|
|
13
|
+
// src/core/pathSafety.ts's relativeWithinRoot for containment/normalization
|
|
14
|
+
// so target-root escape guarantees stay centralized.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const EXCLUDED_DIR_NAMES = new Set([
|
|
17
|
+
".git",
|
|
18
|
+
".gradle",
|
|
19
|
+
"build",
|
|
20
|
+
"out",
|
|
21
|
+
"dist",
|
|
22
|
+
"node_modules",
|
|
23
|
+
"coverage",
|
|
24
|
+
"reports",
|
|
25
|
+
"lab-output",
|
|
26
|
+
".my-dev-kit",
|
|
27
|
+
".idea",
|
|
28
|
+
".vscode",
|
|
29
|
+
".vs",
|
|
30
|
+
]);
|
|
31
|
+
// Walks `root`, returning sorted POSIX-style relative file paths. Directory
|
|
32
|
+
// symlinks are never followed (recorded in `skippedSymlinks` instead), and
|
|
33
|
+
// traversal never leaves `root` (relativeWithinRoot throws on escape, which
|
|
34
|
+
// cannot happen here since paths are built from readdir entries under root).
|
|
35
|
+
export function walkAndroidCandidateFiles(root) {
|
|
36
|
+
const resolvedRoot = path.resolve(root);
|
|
37
|
+
const relativeFilePaths = [];
|
|
38
|
+
const skippedSymlinks = [];
|
|
39
|
+
const visit = (dir) => {
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const fullPath = path.join(dir, entry.name);
|
|
49
|
+
if (entry.isSymbolicLink()) {
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = lstatSync(fullPath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (stat.isSymbolicLink()) {
|
|
58
|
+
skippedSymlinks.push(relativeWithinRoot(resolvedRoot, fullPath));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (EXCLUDED_DIR_NAMES.has(entry.name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
visit(fullPath);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (entry.isFile()) {
|
|
70
|
+
relativeFilePaths.push(relativeWithinRoot(resolvedRoot, fullPath));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
visit(resolvedRoot);
|
|
75
|
+
return {
|
|
76
|
+
relativeFilePaths: relativeFilePaths.sort((a, b) => a.localeCompare(b)),
|
|
77
|
+
skippedSymlinks: skippedSymlinks.sort((a, b) => a.localeCompare(b)),
|
|
78
|
+
};
|
|
79
|
+
}
|