pumuki 6.3.360 → 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.
@@ -7,6 +7,8 @@ import {
7
7
  findKotlinOpenClosedWhenMatch,
8
8
  findKotlinPresentationSrpMatch,
9
9
  hasAndroidAsyncTaskUsage,
10
+ hasAndroidCustomSingletonObjectUsage,
11
+ hasAndroidHiltInjectionWithoutEntryPointUsage,
10
12
  hasAndroidLegacyFingerprintApiUsage,
11
13
  hasKotlinCoroutineTryCatchUsage,
12
14
  hasKotlinDispatcherMainBoundaryLeakUsage,
@@ -1278,3 +1280,55 @@ val sample = "FingerprintManagerCompat"
1278
1280
  assert.equal(hasAndroidLegacyFingerprintApiUsage(legacyCompat), true);
1279
1281
  assert.equal(hasAndroidLegacyFingerprintApiUsage(modern), false);
1280
1282
  });
1283
+
1284
+ test('hasAndroidCustomSingletonObjectUsage detecta singletons Kotlin propios y preserva objetos seguros', () => {
1285
+ const singletonSource = `
1286
+ object CheckoutRepository {
1287
+ fun loadCheckout() = Unit
1288
+ }
1289
+
1290
+ object SessionManager {
1291
+ fun clear() = Unit
1292
+ }
1293
+ `;
1294
+ const safeSource = `
1295
+ object CheckoutRoute {
1296
+ const val PATH = "checkout"
1297
+ }
1298
+
1299
+ @Module
1300
+ @InstallIn(SingletonComponent::class)
1301
+ object CheckoutModule {
1302
+ @Provides
1303
+ fun provideRepository(): CheckoutRepository = CheckoutRepository()
1304
+ }
1305
+ `;
1306
+
1307
+ assert.equal(hasAndroidCustomSingletonObjectUsage(singletonSource), true);
1308
+ assert.equal(hasAndroidCustomSingletonObjectUsage(safeSource), false);
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
+ });
@@ -130,6 +130,96 @@ const sortedUniqueLines = (lines: ReadonlyArray<number>): readonly number[] => {
130
130
  .sort((left, right) => left - right);
131
131
  };
132
132
 
133
+ const androidCustomSingletonObjectNamePattern =
134
+ /(?:Repository|Service|Manager|Client|Store|DataSource|Gateway|Controller|Coordinator)$/;
135
+
136
+ const androidSafeObjectNamePattern =
137
+ /(?:Route|Routes|Screen|Screens|Destination|Destinations|Module|Modules|Constants|Config|Keys|Theme|Colors|Typography)$/;
138
+
139
+ const isAndroidHiltModuleObjectContext = (
140
+ lines: readonly string[],
141
+ index: number
142
+ ): boolean => {
143
+ const context = lines
144
+ .slice(Math.max(0, index - 4), index)
145
+ .map((line) => stripKotlinLineForSemanticScan(line))
146
+ .join('\n');
147
+ return /@(?:Module|InstallIn|Provides|Binds)\b/.test(context);
148
+ };
149
+
150
+ export const collectAndroidCustomSingletonObjectLines = (source: string): readonly number[] => {
151
+ const lines = source.split(/\r?\n/);
152
+ const matches: number[] = [];
153
+
154
+ lines.forEach((line, index) => {
155
+ const sanitized = stripKotlinLineForSemanticScan(line);
156
+ if (sanitized.trimStart().startsWith('import ')) {
157
+ return;
158
+ }
159
+ const objectMatch = sanitized.match(/^\s*object\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
160
+ const objectName = objectMatch?.[1];
161
+ if (!objectName) {
162
+ return;
163
+ }
164
+ if (androidSafeObjectNamePattern.test(objectName) || isAndroidHiltModuleObjectContext(lines, index)) {
165
+ return;
166
+ }
167
+ if (androidCustomSingletonObjectNamePattern.test(objectName)) {
168
+ matches.push(index + 1);
169
+ }
170
+ });
171
+
172
+ return sortedUniqueLines(matches);
173
+ };
174
+
175
+ export const hasAndroidCustomSingletonObjectUsage = (source: string): boolean =>
176
+ collectAndroidCustomSingletonObjectLines(source).length > 0;
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
+
133
223
  const countTokenOccurrences = (line: string, token: string): number => {
134
224
  return line.split(token).length - 1;
135
225
  };
@@ -894,6 +894,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
894
894
  { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinRunBlockingUsage, ruleId: 'heuristics.android.run-blocking.ast', code: 'HEURISTICS_ANDROID_RUN_BLOCKING_AST', message: 'AST heuristic detected runBlocking usage in production Kotlin code.' },
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
+ { 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.' },
897
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.' },
898
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.' },
899
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, 40);
6
+ assert.equal(androidRules.length, 42);
7
7
 
8
8
  const ids = androidRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -14,6 +14,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
14
14
  'heuristics.android.flow.viewmodel-flow-without-statein.ast',
15
15
  'heuristics.android.flow.sharedflow-used-as-state.ast',
16
16
  'heuristics.android.security.legacy-fingerprint-api.ast',
17
+ 'heuristics.android.architecture.custom-singleton-object.ast',
18
+ 'heuristics.android.di.hilt-injection-without-entrypoint.ast',
17
19
  'heuristics.android.concurrency.asynctask.ast',
18
20
  'heuristics.android.architecture.god-activity.ast',
19
21
  'heuristics.android.startup.application-oncreate-heavy-init.ast',
@@ -88,6 +90,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
88
90
  byId.get('heuristics.android.security.legacy-fingerprint-api.ast')?.then.code,
89
91
  'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST'
90
92
  );
93
+ assert.equal(
94
+ byId.get('heuristics.android.architecture.custom-singleton-object.ast')?.then.code,
95
+ 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST'
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
+ );
91
101
  assert.equal(
92
102
  byId.get('heuristics.android.concurrency.asynctask.ast')?.then.code,
93
103
  'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST'
@@ -135,6 +135,46 @@ export const androidRules: RuleSet = [
135
135
  code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST',
136
136
  },
137
137
  },
138
+ {
139
+ id: 'heuristics.android.architecture.custom-singleton-object.ast',
140
+ description:
141
+ 'Detects custom Kotlin object singletons in Android production code where Hilt/Dagger dependency injection is expected.',
142
+ severity: 'WARN',
143
+ platform: 'android',
144
+ locked: true,
145
+ when: {
146
+ kind: 'Heuristic',
147
+ where: {
148
+ ruleId: 'heuristics.android.architecture.custom-singleton-object.ast',
149
+ },
150
+ },
151
+ then: {
152
+ kind: 'Finding',
153
+ message:
154
+ 'AST heuristic detected a custom Kotlin object singleton in Android production code; use Hilt/Dagger dependency injection.',
155
+ code: 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST',
156
+ },
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
+ },
138
178
  {
139
179
  id: 'heuristics.android.concurrency.asynctask.ast',
140
180
  description:
@@ -1435,6 +1435,18 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
1435
1435
  'android.security.biometric-prompt',
1436
1436
  ['heuristics.android.security.legacy-fingerprint-api.ast']
1437
1437
  ),
1438
+ 'skills.android.guideline.android.no-singleton-usar-inyeccio-n-de-dependencias-hilt-dagger':
1439
+ heuristicDetector('android.architecture.custom-singleton-object', [
1440
+ 'heuristics.android.architecture.custom-singleton-object.ast',
1441
+ ]),
1442
+ 'skills.android.guideline.android.singletons-everywhere-usar-hilt-di': heuristicDetector(
1443
+ 'android.architecture.custom-singleton-object',
1444
+ ['heuristics.android.architecture.custom-singleton-object.ast']
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
+ ]),
1438
1450
  'skills.android.guideline.android.statein-convertir-cold-flow-a-hot-stateflow': heuristicDetector(
1439
1451
  'android.flow.viewmodel-flow-without-statein',
1440
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.360",
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": {
package/skills.lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "compilerVersion": "1.0.0",
4
- "generatedAt": "2026-05-24T21:08:57.332Z",
4
+ "generatedAt": "2026-05-24T21:20:37.941Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",