pumuki 6.3.361 → 6.3.362
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/core/facts/detectors/text/android.test.ts +26 -0
- package/core/facts/detectors/text/android.ts +45 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/android.test.ts +6 -1
- package/core/rules/presets/heuristics/android.ts +20 -0
- package/integrations/config/skillsDetectorRegistry.ts +4 -0
- package/package.json +1 -1
- package/skills.lock.json +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
9
|
hasAndroidAsyncTaskUsage,
|
|
10
10
|
hasAndroidCustomSingletonObjectUsage,
|
|
11
|
+
hasAndroidHiltInjectionWithoutEntryPointUsage,
|
|
11
12
|
hasAndroidLegacyFingerprintApiUsage,
|
|
12
13
|
hasKotlinCoroutineTryCatchUsage,
|
|
13
14
|
hasKotlinDispatcherMainBoundaryLeakUsage,
|
|
@@ -1306,3 +1307,28 @@ object CheckoutModule {
|
|
|
1306
1307
|
assert.equal(hasAndroidCustomSingletonObjectUsage(singletonSource), true);
|
|
1307
1308
|
assert.equal(hasAndroidCustomSingletonObjectUsage(safeSource), false);
|
|
1308
1309
|
});
|
|
1310
|
+
|
|
1311
|
+
test('hasAndroidHiltInjectionWithoutEntryPointUsage detecta Activity o Fragment con inyección Hilt sin AndroidEntryPoint', () => {
|
|
1312
|
+
const missingEntryPoint = `
|
|
1313
|
+
class CheckoutActivity : AppCompatActivity() {
|
|
1314
|
+
@Inject lateinit var analytics: CheckoutAnalytics
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
class StoresFragment : Fragment() {
|
|
1318
|
+
@Inject lateinit var repository: StoresRepository
|
|
1319
|
+
}
|
|
1320
|
+
`;
|
|
1321
|
+
const validEntryPoint = `
|
|
1322
|
+
@AndroidEntryPoint
|
|
1323
|
+
class CheckoutActivity : AppCompatActivity() {
|
|
1324
|
+
@Inject lateinit var analytics: CheckoutAnalytics
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
class CheckoutViewModel @Inject constructor(
|
|
1328
|
+
private val repository: CheckoutRepository
|
|
1329
|
+
) : ViewModel()
|
|
1330
|
+
`;
|
|
1331
|
+
|
|
1332
|
+
assert.equal(hasAndroidHiltInjectionWithoutEntryPointUsage(missingEntryPoint), true);
|
|
1333
|
+
assert.equal(hasAndroidHiltInjectionWithoutEntryPointUsage(validEntryPoint), false);
|
|
1334
|
+
});
|
|
@@ -175,6 +175,51 @@ export const collectAndroidCustomSingletonObjectLines = (source: string): readon
|
|
|
175
175
|
export const hasAndroidCustomSingletonObjectUsage = (source: string): boolean =>
|
|
176
176
|
collectAndroidCustomSingletonObjectLines(source).length > 0;
|
|
177
177
|
|
|
178
|
+
export const collectAndroidHiltInjectionWithoutEntryPointLines = (
|
|
179
|
+
source: string
|
|
180
|
+
): readonly number[] => {
|
|
181
|
+
const lines = source.split(/\r?\n/);
|
|
182
|
+
const matches: number[] = [];
|
|
183
|
+
|
|
184
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
185
|
+
const sanitized = stripKotlinLineForSemanticScan(lines[index] ?? '');
|
|
186
|
+
const classMatch = sanitized.match(
|
|
187
|
+
/^\s*(?:class|open\s+class)\s+([A-Za-z_][A-Za-z0-9_]*)\b[^{]*(?:Activity|Fragment)\s*\(/
|
|
188
|
+
);
|
|
189
|
+
if (!classMatch) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const annotationContext = lines
|
|
194
|
+
.slice(Math.max(0, index - 4), index)
|
|
195
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
196
|
+
.join('\n');
|
|
197
|
+
if (/@AndroidEntryPoint\b/.test(annotationContext)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const classStartLine = index + 1;
|
|
202
|
+
let braceDepth =
|
|
203
|
+
countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
|
|
204
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
205
|
+
const candidate = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
|
|
206
|
+
if (/@Inject\b\s+lateinit\s+var\b/.test(candidate)) {
|
|
207
|
+
matches.push(classStartLine, cursor + 1);
|
|
208
|
+
}
|
|
209
|
+
braceDepth += countTokenOccurrences(candidate, '{');
|
|
210
|
+
braceDepth -= countTokenOccurrences(candidate, '}');
|
|
211
|
+
if (braceDepth <= 0) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return sortedUniqueLines(matches);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const hasAndroidHiltInjectionWithoutEntryPointUsage = (source: string): boolean =>
|
|
221
|
+
collectAndroidHiltInjectionWithoutEntryPointLines(source).length > 0;
|
|
222
|
+
|
|
178
223
|
const countTokenOccurrences = (line: string, token: string): number => {
|
|
179
224
|
return line.split(token).length - 1;
|
|
180
225
|
};
|
|
@@ -895,6 +895,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
895
895
|
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidAsyncTaskUsage, ruleId: 'heuristics.android.concurrency.asynctask.ast', code: 'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST', message: 'AST heuristic detected deprecated AsyncTask usage in Android production code; use coroutines.' },
|
|
896
896
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidLegacyFingerprintApiUsage, locateLines: TextAndroid.collectAndroidLegacyFingerprintApiLines, ruleId: 'heuristics.android.security.legacy-fingerprint-api.ast', code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST', message: 'AST heuristic detected legacy FingerprintManager API usage; use androidx.biometric.BiometricPrompt.' },
|
|
897
897
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidCustomSingletonObjectUsage, locateLines: TextAndroid.collectAndroidCustomSingletonObjectLines, primaryNode: (lines) => ({ kind: 'class', name: 'Kotlin object singleton', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: Hilt/Dagger dependency injection boundary', lines }], why: 'Kotlin object singletons create global mutable architecture boundaries that bypass the Android DI graph.', impact: 'Global repositories, services or managers make feature slices harder to test, override and isolate in brownfield remediation.', expected_fix: 'Replace custom Kotlin object singletons with constructor-injected classes provided by Hilt/Dagger. Keep object declarations only for constants, routes, UI metadata or DI modules.', ruleId: 'heuristics.android.architecture.custom-singleton-object.ast', code: 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST', message: 'AST heuristic detected a custom Kotlin object singleton in Android production code; use Hilt/Dagger dependency injection.' },
|
|
898
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidHiltInjectionWithoutEntryPointUsage, locateLines: TextAndroid.collectAndroidHiltInjectionWithoutEntryPointLines, primaryNode: (lines) => ({ kind: 'class', name: 'Activity/Fragment using Hilt injection without @AndroidEntryPoint', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: annotate Activity/Fragment with @AndroidEntryPoint', lines }], why: 'Hilt field injection in Activity or Fragment requires the Android entry point annotation on the Android framework type.', impact: 'The app can compile but crash or fail injection at runtime because the generated Hilt component is not installed for that entry point.', expected_fix: 'Add @AndroidEntryPoint to the Activity or Fragment that declares @Inject fields, or remove field injection and use constructor/ViewModel boundaries where appropriate.', ruleId: 'heuristics.android.di.hilt-injection-without-entrypoint.ast', code: 'HEURISTICS_ANDROID_DI_HILT_INJECTION_WITHOUT_ENTRYPOINT_AST', message: 'AST heuristic detected Hilt field injection in Activity/Fragment without @AndroidEntryPoint.' },
|
|
898
899
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGodActivityUsage, ruleId: 'heuristics.android.architecture.god-activity.ast', code: 'HEURISTICS_ANDROID_ARCHITECTURE_GOD_ACTIVITY_AST', message: 'AST heuristic detected an Android Activity mixing UI entrypoint with product responsibilities; keep Activity thin and move features to composables/ViewModels/use cases.' },
|
|
899
900
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinApplicationOnCreateHeavyInitializationUsage, ruleId: 'heuristics.android.startup.application-oncreate-heavy-init.ast', code: 'HEURISTICS_ANDROID_STARTUP_APPLICATION_ONCREATE_HEAVY_INIT_AST', message: 'AST heuristic detected heavy library initialization in Application.onCreate; move lazy startup work to AndroidX Startup Initializer or defer it behind the feature boundary.' },
|
|
900
901
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinNonLazyScrollableCollectionUsage, ruleId: 'heuristics.android.compose.non-lazy-scrollable-collection.ast', code: 'HEURISTICS_ANDROID_COMPOSE_NON_LAZY_SCROLLABLE_COLLECTION_AST', message: 'AST heuristic detected a scrollable Column/Row rendering a collection; use LazyColumn/LazyRow for virtualized lists.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { androidRules } from './android';
|
|
4
4
|
|
|
5
5
|
test('androidRules define reglas heurísticas locked para plataforma android', () => {
|
|
6
|
-
assert.equal(androidRules.length,
|
|
6
|
+
assert.equal(androidRules.length, 42);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -15,6 +15,7 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
15
15
|
'heuristics.android.flow.sharedflow-used-as-state.ast',
|
|
16
16
|
'heuristics.android.security.legacy-fingerprint-api.ast',
|
|
17
17
|
'heuristics.android.architecture.custom-singleton-object.ast',
|
|
18
|
+
'heuristics.android.di.hilt-injection-without-entrypoint.ast',
|
|
18
19
|
'heuristics.android.concurrency.asynctask.ast',
|
|
19
20
|
'heuristics.android.architecture.god-activity.ast',
|
|
20
21
|
'heuristics.android.startup.application-oncreate-heavy-init.ast',
|
|
@@ -93,6 +94,10 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
93
94
|
byId.get('heuristics.android.architecture.custom-singleton-object.ast')?.then.code,
|
|
94
95
|
'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST'
|
|
95
96
|
);
|
|
97
|
+
assert.equal(
|
|
98
|
+
byId.get('heuristics.android.di.hilt-injection-without-entrypoint.ast')?.then.code,
|
|
99
|
+
'HEURISTICS_ANDROID_DI_HILT_INJECTION_WITHOUT_ENTRYPOINT_AST'
|
|
100
|
+
);
|
|
96
101
|
assert.equal(
|
|
97
102
|
byId.get('heuristics.android.concurrency.asynctask.ast')?.then.code,
|
|
98
103
|
'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST'
|
|
@@ -155,6 +155,26 @@ export const androidRules: RuleSet = [
|
|
|
155
155
|
code: 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST',
|
|
156
156
|
},
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
id: 'heuristics.android.di.hilt-injection-without-entrypoint.ast',
|
|
160
|
+
description:
|
|
161
|
+
'Detects Activity or Fragment Hilt field injection without @AndroidEntryPoint.',
|
|
162
|
+
severity: 'WARN',
|
|
163
|
+
platform: 'android',
|
|
164
|
+
locked: true,
|
|
165
|
+
when: {
|
|
166
|
+
kind: 'Heuristic',
|
|
167
|
+
where: {
|
|
168
|
+
ruleId: 'heuristics.android.di.hilt-injection-without-entrypoint.ast',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
then: {
|
|
172
|
+
kind: 'Finding',
|
|
173
|
+
message:
|
|
174
|
+
'AST heuristic detected Hilt field injection in Activity/Fragment without @AndroidEntryPoint.',
|
|
175
|
+
code: 'HEURISTICS_ANDROID_DI_HILT_INJECTION_WITHOUT_ENTRYPOINT_AST',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
158
178
|
{
|
|
159
179
|
id: 'heuristics.android.concurrency.asynctask.ast',
|
|
160
180
|
description:
|
|
@@ -1443,6 +1443,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
1443
1443
|
'android.architecture.custom-singleton-object',
|
|
1444
1444
|
['heuristics.android.architecture.custom-singleton-object.ast']
|
|
1445
1445
|
),
|
|
1446
|
+
'skills.android.guideline.android.androidentrypoint-activity-fragment-viewmodel':
|
|
1447
|
+
heuristicDetector('android.di.hilt-injection-without-entrypoint', [
|
|
1448
|
+
'heuristics.android.di.hilt-injection-without-entrypoint.ast',
|
|
1449
|
+
]),
|
|
1446
1450
|
'skills.android.guideline.android.statein-convertir-cold-flow-a-hot-stateflow': heuristicDetector(
|
|
1447
1451
|
'android.flow.viewmodel-flow-without-statein',
|
|
1448
1452
|
['heuristics.android.flow.viewmodel-flow-without-statein.ast']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.362",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|