pumuki-ast-hooks 5.5.50 → 5.5.52
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/docs/MCP_SERVERS.md +16 -2
- package/docs/RELEASE_NOTES.md +34 -0
- package/hooks/git-status-monitor.ts +0 -5
- package/hooks/notify-macos.ts +0 -1
- package/hooks/pre-tool-use-evidence-validator.ts +0 -1
- package/package.json +2 -2
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +2 -4
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +2 -20
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -139
- package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +1 -13
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +17 -9
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +2 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +397 -34
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +398 -1
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +0 -16
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +14 -25
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +20 -76
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +0 -350
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
const { pushFinding } = require('../../ast-core');
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const {
|
|
17
|
+
buildContext,
|
|
18
|
+
checkPackageSwiftExists,
|
|
19
|
+
checkFeatureModulesStructure,
|
|
20
|
+
checkCoreModulesStructure,
|
|
21
|
+
checkPublicAPIExposure,
|
|
22
|
+
checkModuleDependencies,
|
|
23
|
+
checkCrossModuleViolations,
|
|
24
|
+
checkPackageSwiftConfiguration,
|
|
25
|
+
checkTargetNaming,
|
|
26
|
+
checkProductConfiguration,
|
|
27
|
+
checkDependencyVersions,
|
|
28
|
+
checkTestTargets,
|
|
29
|
+
checkModuleBoundaries,
|
|
30
|
+
} = require('./iOSSPMChecks');
|
|
16
31
|
|
|
17
32
|
class iOSSPMRules {
|
|
18
33
|
constructor(findings, projectRoot) {
|
|
@@ -21,450 +36,29 @@ class iOSSPMRules {
|
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
analyze() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.checkTargetNaming();
|
|
32
|
-
this.checkProductConfiguration();
|
|
33
|
-
this.checkDependencyVersions();
|
|
34
|
-
this.checkTestTargets();
|
|
35
|
-
this.checkModuleBoundaries();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 1. Package.swift debe existir para modularización
|
|
40
|
-
*/
|
|
41
|
-
checkPackageSwiftExists() {
|
|
42
|
-
const packagePath = path.join(this.projectRoot, 'Package.swift');
|
|
43
|
-
|
|
44
|
-
if (!fs.existsSync(packagePath)) {
|
|
45
|
-
const swiftFiles = this.findSwiftFiles();
|
|
46
|
-
|
|
47
|
-
if (swiftFiles.length > 50) {
|
|
48
|
-
pushFinding(this.findings, {
|
|
49
|
-
ruleId: 'ios.spm.missing_package_swift',
|
|
50
|
-
severity: 'medium',
|
|
51
|
-
message: `Proyecto con ${swiftFiles.length} archivos Swift sin Package.swift. Considerar modularización con SPM.`,
|
|
52
|
-
filePath: 'PROJECT_ROOT',
|
|
53
|
-
line: 1,
|
|
54
|
-
suggestion: `Crear Package.swift para modularizar:
|
|
55
|
-
|
|
56
|
-
swift package init --type library
|
|
57
|
-
swift package init --type executable`
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
this.packageSwiftPath = packagePath;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 2. Feature modules structure
|
|
67
|
-
*/
|
|
68
|
-
checkFeatureModulesStructure() {
|
|
69
|
-
const sources = path.join(this.projectRoot, 'Sources');
|
|
70
|
-
|
|
71
|
-
if (!fs.existsSync(sources)) return;
|
|
72
|
-
|
|
73
|
-
const modules = fs.readdirSync(sources).filter(dir => {
|
|
74
|
-
const stats = fs.statSync(path.join(sources, dir));
|
|
75
|
-
return stats.isDirectory();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const featureModules = modules.filter(m => m.startsWith('Feature'));
|
|
79
|
-
const coreModules = modules.filter(m => m.startsWith('Core'));
|
|
80
|
-
|
|
81
|
-
if (modules.length > 5 && featureModules.length === 0) {
|
|
82
|
-
pushFinding(this.findings, {
|
|
83
|
-
ruleId: 'ios.spm.missing_feature_modules',
|
|
84
|
-
severity: 'medium',
|
|
85
|
-
message: `${modules.length} módulos sin naming convention Feature*. Considerar renombrar.`,
|
|
86
|
-
filePath: 'Sources/',
|
|
87
|
-
line: 1,
|
|
88
|
-
suggestion: `Naming convention recomendado:
|
|
89
|
-
|
|
90
|
-
Sources/
|
|
91
|
-
├── FeatureOrders/
|
|
92
|
-
├── FeatureUsers/
|
|
93
|
-
├── FeatureAuth/
|
|
94
|
-
├── CoreNetworking/
|
|
95
|
-
├── CoreDatabase/
|
|
96
|
-
└── CoreUI/`
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (modules.length > 10 && coreModules.length === 0) {
|
|
101
|
-
pushFinding(this.findings, {
|
|
102
|
-
ruleId: 'ios.spm.missing_core_modules',
|
|
103
|
-
severity: 'medium',
|
|
104
|
-
message: 'Proyecto grande sin módulos Core*. Considerar extraer código compartido.',
|
|
105
|
-
filePath: 'Sources/',
|
|
106
|
-
line: 1
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 3. Core modules detection
|
|
113
|
-
*/
|
|
114
|
-
checkCoreModulesStructure() {
|
|
115
|
-
const recommendedCoreModules = ['CoreNetworking', 'CoreDatabase', 'CoreUI', 'CoreModels'];
|
|
116
|
-
const sources = path.join(this.projectRoot, 'Sources');
|
|
117
|
-
|
|
118
|
-
if (!fs.existsSync(sources)) return;
|
|
119
|
-
|
|
120
|
-
const existingModules = fs.readdirSync(sources);
|
|
121
|
-
const missingCore = recommendedCoreModules.filter(core => !existingModules.includes(core));
|
|
122
|
-
|
|
123
|
-
if (missingCore.length > 0 && existingModules.length > 8) {
|
|
124
|
-
pushFinding(this.findings, {
|
|
125
|
-
ruleId: 'ios.spm.recommended_core_modules_missing',
|
|
126
|
-
severity: 'low',
|
|
127
|
-
message: `Módulos Core recomendados faltantes: ${missingCore.join(', ')}`,
|
|
128
|
-
filePath: 'Sources/',
|
|
129
|
-
line: 1,
|
|
130
|
-
suggestion: 'CoreNetworking, CoreDatabase, CoreUI ayudan a organizar código compartido'
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 4. Public API exposure control
|
|
137
|
-
*/
|
|
138
|
-
checkPublicAPIExposure() {
|
|
139
|
-
const swiftFiles = this.findSwiftFiles();
|
|
140
|
-
|
|
141
|
-
swiftFiles.forEach(file => {
|
|
142
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
143
|
-
|
|
144
|
-
const publicCount = (content.match(/\bpublic\s+(class|struct|enum|func|var|let)/g) || []).length;
|
|
145
|
-
const totalDeclarations = (content.match(/\b(class|struct|enum|func)\s+\w+/g) || []).length;
|
|
146
|
-
|
|
147
|
-
if (totalDeclarations > 0) {
|
|
148
|
-
const publicPercentage = (publicCount / totalDeclarations) * 100;
|
|
149
|
-
|
|
150
|
-
if (publicPercentage > 70) {
|
|
151
|
-
pushFinding(this.findings, {
|
|
152
|
-
ruleId: 'ios.spm.excessive_public_api',
|
|
153
|
-
severity: 'medium',
|
|
154
|
-
message: `${publicPercentage.toFixed(0)}% de declaraciones son public. Minimizar API pública del módulo.`,
|
|
155
|
-
filePath: file,
|
|
156
|
-
line: 1,
|
|
157
|
-
suggestion: `Usar internal por defecto, public solo para API externa:
|
|
158
|
-
|
|
159
|
-
// ❌ Todo public
|
|
160
|
-
public class Helper { ... }
|
|
161
|
-
public func process() { ... }
|
|
162
|
-
|
|
163
|
-
internal class Helper { ... }
|
|
164
|
-
public func process() { ... } // Solo lo necesario`
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (file.includes('/API/') && !content.includes('public ')) {
|
|
170
|
-
pushFinding(this.findings, {
|
|
171
|
-
ruleId: 'ios.spm.api_module_not_public',
|
|
172
|
-
severity: 'medium',
|
|
173
|
-
message: 'Módulo API sin declaraciones public. API module debe exponer funcionalidad.',
|
|
174
|
-
filePath: file,
|
|
175
|
-
line: 1
|
|
176
|
-
});
|
|
177
|
-
}
|
|
39
|
+
const ctx = buildContext({
|
|
40
|
+
findings: this.findings,
|
|
41
|
+
projectRoot: this.projectRoot,
|
|
42
|
+
packageSwiftPath: this.packageSwiftPath,
|
|
43
|
+
findSwiftFiles: this.findSwiftFiles.bind(this),
|
|
44
|
+
findSwiftFilesInDirectory: this.findSwiftFilesInDirectory.bind(this),
|
|
45
|
+
findLineNumber: this.findLineNumber.bind(this),
|
|
178
46
|
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* 5. Module dependencies validation
|
|
183
|
-
*/
|
|
184
|
-
checkModuleDependencies() {
|
|
185
|
-
if (!this.packageSwiftPath) return;
|
|
186
|
-
|
|
187
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
188
|
-
|
|
189
|
-
const targets = content.match(/\.target\([\s\S]*?name:\s*"([^"]+)"[\s\S]*?dependencies:\s*\[([\s\S]*?)\]/g);
|
|
190
|
-
|
|
191
|
-
if (targets) {
|
|
192
|
-
const dependencies = new Map();
|
|
193
|
-
|
|
194
|
-
targets.forEach(target => {
|
|
195
|
-
const name = target.match(/name:\s*"([^"]+)"/)?.[1];
|
|
196
|
-
const deps = target.match(/dependencies:\s*\[([\s\S]*?)\]/)?.[1];
|
|
197
|
-
|
|
198
|
-
if (name && deps) {
|
|
199
|
-
const depList = deps.match(/"([^"]+)"/g)?.map(d => d.replace(/"/g, '')) || [];
|
|
200
|
-
dependencies.set(name, depList);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
dependencies.forEach((deps, moduleName) => {
|
|
205
|
-
deps.forEach(dep => {
|
|
206
|
-
const depDeps = dependencies.get(dep) || [];
|
|
207
|
-
if (depDeps.includes(moduleName)) {
|
|
208
|
-
pushFinding(this.findings, {
|
|
209
|
-
ruleId: 'ios.spm.circular_dependency',
|
|
210
|
-
severity: 'critical',
|
|
211
|
-
message: `Dependencia circular detectada: ${moduleName} ↔ ${dep}`,
|
|
212
|
-
filePath: 'Package.swift',
|
|
213
|
-
line: 1,
|
|
214
|
-
suggestion: 'Romper dependencia circular extrayendo código compartido a módulo Core'
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
dependencies.forEach((deps, moduleName) => {
|
|
221
|
-
if (moduleName.startsWith('Feature')) {
|
|
222
|
-
const featureDeps = deps.filter(d => d.startsWith('Feature'));
|
|
223
|
-
|
|
224
|
-
if (featureDeps.length > 0) {
|
|
225
|
-
pushFinding(this.findings, {
|
|
226
|
-
ruleId: 'ios.spm.feature_to_feature_dependency',
|
|
227
|
-
severity: 'high',
|
|
228
|
-
message: `Feature module '${moduleName}' depende de otro Feature: ${featureDeps.join(', ')}. Extraer a Core.`,
|
|
229
|
-
filePath: 'Package.swift',
|
|
230
|
-
line: 1,
|
|
231
|
-
suggestion: 'Features NO deben depender entre sí. Usar Core modules para compartir código.'
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 6. Cross-module violations
|
|
241
|
-
*/
|
|
242
|
-
checkCrossModuleViolations() {
|
|
243
|
-
const swiftFiles = this.findSwiftFiles();
|
|
244
|
-
|
|
245
|
-
swiftFiles.forEach(file => {
|
|
246
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
247
|
-
|
|
248
|
-
const internalImports = content.match(/@_implementationOnly\s+import\s+(\w+)/g);
|
|
249
47
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
severity: 'high',
|
|
265
|
-
message: '@testable import en código de producción. Solo debe usarse en tests.',
|
|
266
|
-
filePath: file,
|
|
267
|
-
line: this.findLineNumber(content, '@testable import')
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* 7. Package.swift configuration
|
|
275
|
-
*/
|
|
276
|
-
checkPackageSwiftConfiguration() {
|
|
277
|
-
if (!this.packageSwiftPath) return;
|
|
278
|
-
|
|
279
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
280
|
-
|
|
281
|
-
const toolsVersion = content.match(/\/\/\s*swift-tools-version:\s*(\d+\.\d+)/)?.[1];
|
|
282
|
-
if (toolsVersion && parseFloat(toolsVersion) < 5.9) {
|
|
283
|
-
pushFinding(this.findings, {
|
|
284
|
-
ruleId: 'ios.spm.outdated_tools_version',
|
|
285
|
-
severity: 'medium',
|
|
286
|
-
message: `Swift tools version ${toolsVersion} desactualizado. Actualizar a 5.9+`,
|
|
287
|
-
filePath: 'Package.swift',
|
|
288
|
-
line: 1
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!content.includes('platforms:')) {
|
|
293
|
-
pushFinding(this.findings, {
|
|
294
|
-
ruleId: 'ios.spm.missing_platforms',
|
|
295
|
-
severity: 'medium',
|
|
296
|
-
message: 'Package.swift sin platforms: especificado. Definir versión mínima de iOS.',
|
|
297
|
-
filePath: 'Package.swift',
|
|
298
|
-
line: 1,
|
|
299
|
-
suggestion: 'platforms: [.iOS(.v15), .macOS(.v12)]'
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* 8. Target naming consistency
|
|
306
|
-
*/
|
|
307
|
-
checkTargetNaming() {
|
|
308
|
-
if (!this.packageSwiftPath) return;
|
|
309
|
-
|
|
310
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
311
|
-
const targets = content.match(/\.target\([\s\S]*?name:\s*"([^"]+)"/g);
|
|
312
|
-
|
|
313
|
-
if (targets) {
|
|
314
|
-
const targetNames = targets.map(t => t.match(/name:\s*"([^"]+)"/)?.[1]).filter(Boolean);
|
|
315
|
-
|
|
316
|
-
const hasInconsistentNaming = targetNames.some(name => {
|
|
317
|
-
return name.includes('_') || name.includes('-') || /[a-z][A-Z]/.test(name);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
if (hasInconsistentNaming) {
|
|
321
|
-
pushFinding(this.findings, {
|
|
322
|
-
ruleId: 'ios.spm.inconsistent_target_naming',
|
|
323
|
-
severity: 'low',
|
|
324
|
-
message: 'Naming inconsistente en targets. Usar PascalCase sin separadores.',
|
|
325
|
-
filePath: 'Package.swift',
|
|
326
|
-
line: 1,
|
|
327
|
-
suggestion: 'FeatureOrders, CoreNetworking (no Feature-Orders, Core_Networking)'
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* 9. Product configuration
|
|
335
|
-
*/
|
|
336
|
-
checkProductConfiguration() {
|
|
337
|
-
if (!this.packageSwiftPath) return;
|
|
338
|
-
|
|
339
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
340
|
-
|
|
341
|
-
if (!content.includes('.library(')) {
|
|
342
|
-
pushFinding(this.findings, {
|
|
343
|
-
ruleId: 'ios.spm.missing_products',
|
|
344
|
-
severity: 'medium',
|
|
345
|
-
message: 'Package.swift sin products definidos. Definir libraries para exponer módulos.',
|
|
346
|
-
filePath: 'Package.swift',
|
|
347
|
-
line: 1,
|
|
348
|
-
suggestion: `.library(name: "FeatureOrders", targets: ["FeatureOrders"])`
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* 10. Dependency versions - debe usar versioning semántico
|
|
355
|
-
*/
|
|
356
|
-
checkDependencyVersions() {
|
|
357
|
-
if (!this.packageSwiftPath) return;
|
|
358
|
-
|
|
359
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
360
|
-
|
|
361
|
-
if (content.includes('.branch(')) {
|
|
362
|
-
pushFinding(this.findings, {
|
|
363
|
-
ruleId: 'ios.spm.dependency_branch_instead_version',
|
|
364
|
-
severity: 'high',
|
|
365
|
-
message: 'Dependencia usando .branch() en lugar de versión específica. Usar .upToNextMajor o .exact.',
|
|
366
|
-
filePath: 'Package.swift',
|
|
367
|
-
line: this.findLineNumber(content, '.branch('),
|
|
368
|
-
suggestion: `Usar versiones semánticas:
|
|
369
|
-
|
|
370
|
-
// ❌ Inestable
|
|
371
|
-
.package(url: "...", branch: "main")
|
|
372
|
-
|
|
373
|
-
// ✅ Estable
|
|
374
|
-
.package(url: "...", from: "1.0.0")
|
|
375
|
-
.package(url: "...", .upToNextMajor(from: "1.0.0"))`
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* 11. Test targets - debe haber tests para cada módulo
|
|
382
|
-
*/
|
|
383
|
-
checkTestTargets() {
|
|
384
|
-
if (!this.packageSwiftPath) return;
|
|
385
|
-
|
|
386
|
-
const content = fs.readFileSync(this.packageSwiftPath, 'utf-8');
|
|
387
|
-
|
|
388
|
-
const targets = content.match(/\.target\([\s\S]*?name:\s*"([^"]+)"/g) || [];
|
|
389
|
-
const testTargets = content.match(/\.testTarget\([\s\S]*?name:\s*"([^"]+)"/g) || [];
|
|
390
|
-
|
|
391
|
-
const targetNames = targets.map(t => t.match(/name:\s*"([^"]+)"/)?.[1]).filter(Boolean);
|
|
392
|
-
const testTargetNames = testTargets.map(t => t.match(/name:\s*"([^"]+)"/)?.[1]).filter(Boolean);
|
|
393
|
-
|
|
394
|
-
const targetsWithoutTests = targetNames.filter(name =>
|
|
395
|
-
!testTargetNames.includes(`${name}Tests`) &&
|
|
396
|
-
!name.includes('Tests')
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
if (targetsWithoutTests.length > 0) {
|
|
400
|
-
pushFinding(this.findings, {
|
|
401
|
-
ruleId: 'ios.spm.targets_without_tests',
|
|
402
|
-
severity: 'medium',
|
|
403
|
-
message: `${targetsWithoutTests.length} targets sin test targets: ${targetsWithoutTests.slice(0, 3).join(', ')}`,
|
|
404
|
-
filePath: 'Package.swift',
|
|
405
|
-
line: 1,
|
|
406
|
-
suggestion: `Añadir test targets:
|
|
407
|
-
|
|
408
|
-
.testTarget(
|
|
409
|
-
name: "FeatureOrdersTests",
|
|
410
|
-
dependencies: ["FeatureOrders"]
|
|
411
|
-
)`
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* 12. Module boundaries - detectar imports indebidos
|
|
418
|
-
*/
|
|
419
|
-
checkModuleBoundaries() {
|
|
420
|
-
const sources = path.join(this.projectRoot, 'Sources');
|
|
421
|
-
if (!fs.existsSync(sources)) return;
|
|
422
|
-
|
|
423
|
-
const modules = fs.readdirSync(sources).filter(dir => {
|
|
424
|
-
const stats = fs.statSync(path.join(sources, dir));
|
|
425
|
-
return stats.isDirectory();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
modules.forEach(moduleName => {
|
|
429
|
-
const modulePath = path.join(sources, moduleName);
|
|
430
|
-
const swiftFiles = this.findSwiftFilesInDirectory(modulePath);
|
|
431
|
-
|
|
432
|
-
swiftFiles.forEach(file => {
|
|
433
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
434
|
-
|
|
435
|
-
if (moduleName.startsWith('Feature')) {
|
|
436
|
-
modules.forEach(otherModule => {
|
|
437
|
-
if (otherModule.startsWith('Feature') && otherModule !== moduleName) {
|
|
438
|
-
if (content.includes(`import ${otherModule}`)) {
|
|
439
|
-
pushFinding(this.findings, {
|
|
440
|
-
ruleId: 'ios.spm.feature_imports_feature',
|
|
441
|
-
severity: 'high',
|
|
442
|
-
message: `Feature module '${moduleName}' importa otro Feature '${otherModule}'. Violación de boundaries.`,
|
|
443
|
-
filePath: file,
|
|
444
|
-
line: this.findLineNumber(content, `import ${otherModule}`),
|
|
445
|
-
suggestion: 'Extraer código compartido a Core module'
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (moduleName.startsWith('Core')) {
|
|
453
|
-
modules.forEach(featureModule => {
|
|
454
|
-
if (featureModule.startsWith('Feature') && content.includes(`import ${featureModule}`)) {
|
|
455
|
-
pushFinding(this.findings, {
|
|
456
|
-
ruleId: 'ios.spm.core_imports_feature',
|
|
457
|
-
severity: 'critical',
|
|
458
|
-
message: `Core module '${moduleName}' importa Feature '${featureModule}'. Dependencia invertida!`,
|
|
459
|
-
filePath: file,
|
|
460
|
-
line: this.findLineNumber(content, `import ${featureModule}`),
|
|
461
|
-
suggestion: 'Core NO debe depender de Features. Invertir dependencia.'
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
});
|
|
48
|
+
checkPackageSwiftExists(ctx);
|
|
49
|
+
checkFeatureModulesStructure(ctx);
|
|
50
|
+
checkCoreModulesStructure(ctx);
|
|
51
|
+
checkPublicAPIExposure(ctx);
|
|
52
|
+
checkModuleDependencies(ctx);
|
|
53
|
+
checkCrossModuleViolations(ctx);
|
|
54
|
+
checkPackageSwiftConfiguration(ctx);
|
|
55
|
+
checkTargetNaming(ctx);
|
|
56
|
+
checkProductConfiguration(ctx);
|
|
57
|
+
checkDependencyVersions(ctx);
|
|
58
|
+
checkTestTargets(ctx);
|
|
59
|
+
checkModuleBoundaries(ctx);
|
|
60
|
+
|
|
61
|
+
this.packageSwiftPath = ctx.getPackageSwiftPath();
|
|
468
62
|
}
|
|
469
63
|
|
|
470
64
|
findSwiftFiles() {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
class SourceKittenExtractor {
|
|
2
|
+
extractClasses(ast) {
|
|
3
|
+
const classes = [];
|
|
4
|
+
this.traverse(ast?.substructure, (node) => {
|
|
5
|
+
const kind = node['key.kind'];
|
|
6
|
+
if (kind === 'source.lang.swift.decl.class') {
|
|
7
|
+
classes.push({
|
|
8
|
+
name: node['key.name'],
|
|
9
|
+
line: node['key.line'],
|
|
10
|
+
column: node['key.column'],
|
|
11
|
+
accessibility: node['key.accessibility'],
|
|
12
|
+
inheritedTypes: node['key.inheritedtypes'] || [],
|
|
13
|
+
substructure: node['key.substructure'] || [],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return classes;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
extractFunctions(ast) {
|
|
21
|
+
const functions = [];
|
|
22
|
+
const functionKinds = new Set([
|
|
23
|
+
'source.lang.swift.decl.function.method.instance',
|
|
24
|
+
'source.lang.swift.decl.function.method.class',
|
|
25
|
+
'source.lang.swift.decl.function.method.static',
|
|
26
|
+
'source.lang.swift.decl.function.free',
|
|
27
|
+
]);
|
|
28
|
+
this.traverse(ast?.substructure, (node) => {
|
|
29
|
+
const kind = node['key.kind'];
|
|
30
|
+
if (functionKinds.has(kind)) {
|
|
31
|
+
functions.push({
|
|
32
|
+
name: node['key.name'],
|
|
33
|
+
line: node['key.line'],
|
|
34
|
+
column: node['key.column'],
|
|
35
|
+
kind,
|
|
36
|
+
accessibility: node['key.accessibility'],
|
|
37
|
+
typename: node['key.typename'],
|
|
38
|
+
length: node['key.length'],
|
|
39
|
+
bodyLength: node['key.bodylength'],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return functions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
extractProperties(ast) {
|
|
47
|
+
const properties = [];
|
|
48
|
+
const propertyKinds = new Set([
|
|
49
|
+
'source.lang.swift.decl.var.instance',
|
|
50
|
+
'source.lang.swift.decl.var.class',
|
|
51
|
+
'source.lang.swift.decl.var.static',
|
|
52
|
+
]);
|
|
53
|
+
this.traverse(ast?.substructure, (node) => {
|
|
54
|
+
const kind = node['key.kind'];
|
|
55
|
+
if (propertyKinds.has(kind)) {
|
|
56
|
+
properties.push({
|
|
57
|
+
name: node['key.name'],
|
|
58
|
+
line: node['key.line'],
|
|
59
|
+
column: node['key.column'],
|
|
60
|
+
kind,
|
|
61
|
+
typename: node['key.typename'],
|
|
62
|
+
accessibility: node['key.accessibility'],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return properties;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
extractProtocols(ast) {
|
|
70
|
+
const protocols = [];
|
|
71
|
+
this.traverse(ast?.substructure, (node) => {
|
|
72
|
+
const kind = node['key.kind'];
|
|
73
|
+
if (kind === 'source.lang.swift.decl.protocol') {
|
|
74
|
+
protocols.push({
|
|
75
|
+
name: node['key.name'],
|
|
76
|
+
line: node['key.line'],
|
|
77
|
+
column: node['key.column'],
|
|
78
|
+
accessibility: node['key.accessibility'],
|
|
79
|
+
inheritedTypes: node['key.inheritedtypes'] || [],
|
|
80
|
+
substructure: node['key.substructure'] || [],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return protocols;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
usesSwiftUI(ast) {
|
|
88
|
+
const hasViewProtocol = (nodes) => {
|
|
89
|
+
if (!Array.isArray(nodes)) return false;
|
|
90
|
+
return nodes.some(node => {
|
|
91
|
+
const inheritedTypes = node['key.inheritedtypes'] || [];
|
|
92
|
+
if (inheritedTypes.some(t => t['key.name'] === 'View')) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return hasViewProtocol(node['key.substructure'] || []);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
return hasViewProtocol(ast?.substructure);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
usesUIKit(ast) {
|
|
102
|
+
const hasUIKitBase = (nodes) => {
|
|
103
|
+
if (!Array.isArray(nodes)) return false;
|
|
104
|
+
return nodes.some(node => {
|
|
105
|
+
const inheritedTypes = node['key.inheritedtypes'] || [];
|
|
106
|
+
if (inheritedTypes.some(t =>
|
|
107
|
+
t['key.name'] === 'UIViewController' ||
|
|
108
|
+
t['key.name'] === 'UIView'
|
|
109
|
+
)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return hasUIKitBase(node['key.substructure'] || []);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
return hasUIKitBase(ast?.substructure);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
detectForceUnwraps(_syntaxMap, fileContent) {
|
|
119
|
+
const forceUnwraps = [];
|
|
120
|
+
const lines = (fileContent || '').split('\n');
|
|
121
|
+
lines.forEach((line, index) => {
|
|
122
|
+
const matches = [...line.matchAll(/(\w+)\s*!/g)];
|
|
123
|
+
matches.forEach(match => {
|
|
124
|
+
forceUnwraps.push({
|
|
125
|
+
line: index + 1,
|
|
126
|
+
column: match.index + 1,
|
|
127
|
+
variable: match[1],
|
|
128
|
+
context: line.trim(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
return forceUnwraps;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
traverse(nodes, visitor) {
|
|
136
|
+
if (!Array.isArray(nodes)) return;
|
|
137
|
+
nodes.forEach(node => {
|
|
138
|
+
visitor(node);
|
|
139
|
+
if (node['key.substructure']) {
|
|
140
|
+
this.traverse(node['key.substructure'], visitor);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = SourceKittenExtractor;
|