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
|
@@ -25,7 +25,6 @@ AUTO_MERGE_PR=${GITFLOW_AUTO_MERGE:-false}
|
|
|
25
25
|
PR_BASE_BRANCH=${GITFLOW_PR_BASE:-develop}
|
|
26
26
|
STRICT_ATOMIC=${GITFLOW_STRICT_ATOMIC:-true}
|
|
27
27
|
REQUIRE_TEST_RELATIONS=${GITFLOW_REQUIRE_TESTS:-true}
|
|
28
|
-
STRICT_CHECK=${GITFLOW_STRICT_CHECK:-false}
|
|
29
28
|
|
|
30
29
|
print_section() {
|
|
31
30
|
printf "${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
|
|
@@ -168,17 +167,16 @@ verify_atomic_commit() {
|
|
|
168
167
|
return 0
|
|
169
168
|
fi
|
|
170
169
|
|
|
171
|
-
local files=()
|
|
170
|
+
local -a files=()
|
|
172
171
|
while IFS= read -r file; do
|
|
173
|
-
[[ -
|
|
174
|
-
files+=("$file")
|
|
172
|
+
[[ -n "$file" ]] && files+=("$file")
|
|
175
173
|
done < <($GIT_BIN diff --name-only "${commit}^..${commit}")
|
|
176
174
|
if [[ "${#files[@]}" -eq 0 ]]; then
|
|
177
175
|
return 0
|
|
178
176
|
fi
|
|
179
177
|
|
|
180
|
-
local roots_list
|
|
181
|
-
|
|
178
|
+
local roots_list=""
|
|
179
|
+
local root_count=0
|
|
182
180
|
for file in "${files[@]}"; do
|
|
183
181
|
local root="${file%%/*}"
|
|
184
182
|
if [[ "$root" == "$file" ]]; then
|
|
@@ -193,36 +191,14 @@ verify_atomic_commit() {
|
|
|
193
191
|
;;
|
|
194
192
|
esac
|
|
195
193
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for existing in "${roots_list[@]}"; do
|
|
200
|
-
if [[ "$existing" == "$root" ]]; then
|
|
201
|
-
seen=1
|
|
202
|
-
break
|
|
203
|
-
fi
|
|
204
|
-
done
|
|
205
|
-
fi
|
|
206
|
-
if [[ "$seen" -eq 0 ]]; then
|
|
207
|
-
roots_list+=("$root")
|
|
194
|
+
if [[ " $roots_list " != *" $root "* ]]; then
|
|
195
|
+
roots_list="${roots_list}${root} "
|
|
196
|
+
root_count=$((root_count + 1))
|
|
208
197
|
fi
|
|
209
198
|
done
|
|
210
199
|
|
|
211
|
-
local root_count=${#roots_list[@]}
|
|
212
200
|
if (( root_count > 1 )); then
|
|
213
|
-
|
|
214
|
-
local has_tests=0
|
|
215
|
-
for root in "${roots_list[@]}"; do
|
|
216
|
-
[[ "$root" == "scripts" ]] && has_scripts=1
|
|
217
|
-
[[ "$root" == "tests" ]] && has_tests=1
|
|
218
|
-
done
|
|
219
|
-
|
|
220
|
-
if [[ $has_scripts -eq 1 && $has_tests -eq 1 && $root_count -eq 2 ]]; then
|
|
221
|
-
printf "${GREEN}✅ Commit %s toca scripts + tests (permitido para bugfixes/features con tests).${NC}\n" "$commit"
|
|
222
|
-
return 0
|
|
223
|
-
fi
|
|
224
|
-
|
|
225
|
-
printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$(printf "%s " "${roots_list[@]}")"
|
|
201
|
+
printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$roots_list"
|
|
226
202
|
return 1
|
|
227
203
|
fi
|
|
228
204
|
if (( root_count == 0 )); then
|
|
@@ -230,7 +206,7 @@ verify_atomic_commit() {
|
|
|
230
206
|
return 0
|
|
231
207
|
fi
|
|
232
208
|
local root_name
|
|
233
|
-
for root_name in
|
|
209
|
+
for root_name in $roots_list; do
|
|
234
210
|
printf "${GREEN}✅ Commit %s cumple atomicidad (raíz %s).${NC}\n" "$commit" "$root_name"
|
|
235
211
|
done
|
|
236
212
|
return 0
|
|
@@ -250,10 +226,9 @@ verify_pending_commits_atomic() {
|
|
|
250
226
|
return $?
|
|
251
227
|
fi
|
|
252
228
|
|
|
253
|
-
local commits=()
|
|
229
|
+
local -a commits=()
|
|
254
230
|
while IFS= read -r commit; do
|
|
255
|
-
[[ -
|
|
256
|
-
commits+=("$commit")
|
|
231
|
+
[[ -n "$commit" ]] && commits+=("$commit")
|
|
257
232
|
done < <($GIT_BIN rev-list "${base_ref}..${branch}")
|
|
258
233
|
local failed=0
|
|
259
234
|
for commit in "${commits[@]}"; do
|
|
@@ -384,50 +359,22 @@ cmd_check() {
|
|
|
384
359
|
local branch
|
|
385
360
|
branch=$(current_branch)
|
|
386
361
|
printf "${CYAN}📍 Rama actual: %s${NC}\n" "$branch"
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
ensure_evidence_fresh || failed=1
|
|
391
|
-
lint_hooks_system || failed=1
|
|
392
|
-
run_mobile_checks || failed=1
|
|
393
|
-
else
|
|
394
|
-
ensure_evidence_fresh || true
|
|
395
|
-
lint_hooks_system || true
|
|
396
|
-
run_mobile_checks || true
|
|
397
|
-
fi
|
|
362
|
+
ensure_evidence_fresh || true
|
|
363
|
+
lint_hooks_system || true
|
|
364
|
+
run_mobile_checks || true
|
|
398
365
|
print_sync_table
|
|
399
366
|
print_cleanup_candidates
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
verify_related_files_commit "HEAD" || failed=1
|
|
404
|
-
fi
|
|
405
|
-
if ! verify_pending_commits_atomic "$branch"; then
|
|
406
|
-
failed=1
|
|
407
|
-
fi
|
|
408
|
-
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
409
|
-
if ! verify_pending_commits_related "$branch"; then
|
|
410
|
-
failed=1
|
|
411
|
-
fi
|
|
412
|
-
fi
|
|
413
|
-
else
|
|
414
|
-
verify_atomic_commit "HEAD" || true
|
|
415
|
-
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
416
|
-
verify_related_files_commit "HEAD" || true
|
|
417
|
-
fi
|
|
367
|
+
verify_atomic_commit "HEAD" || true
|
|
368
|
+
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
369
|
+
verify_related_files_commit "HEAD" || true
|
|
418
370
|
fi
|
|
419
371
|
local pending
|
|
420
372
|
pending=$(unpushed_commits "$branch")
|
|
421
373
|
if [[ "$pending" != "0" ]]; then
|
|
422
374
|
printf "${YELLOW}⚠️ Commits sin subir (${pending}). Ejecuta git push.${NC}\n"
|
|
423
|
-
if [[ "${STRICT_CHECK}" == "true" ]]; then
|
|
424
|
-
failed=1
|
|
425
|
-
fi
|
|
426
375
|
else
|
|
427
376
|
printf "${GREEN}✅ No hay commits pendientes de push.${NC}\n"
|
|
428
377
|
fi
|
|
429
|
-
|
|
430
|
-
return $failed
|
|
431
378
|
}
|
|
432
379
|
|
|
433
380
|
cmd_cycle() {
|
|
@@ -580,7 +527,6 @@ main() {
|
|
|
580
527
|
;;
|
|
581
528
|
esac
|
|
582
529
|
}
|
|
583
|
-
|
|
584
530
|
is_test_file() {
|
|
585
531
|
local file="$1"
|
|
586
532
|
case "$file" in
|
|
@@ -681,14 +627,12 @@ verify_pending_commits_related() {
|
|
|
681
627
|
local base_ref="origin/${branch}"
|
|
682
628
|
|
|
683
629
|
if ! $GIT_BIN show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
|
|
684
|
-
verify_related_files_commit "HEAD"
|
|
685
|
-
return $?
|
|
630
|
+
return verify_related_files_commit "HEAD"
|
|
686
631
|
fi
|
|
687
632
|
|
|
688
|
-
local commits=()
|
|
633
|
+
local -a commits=()
|
|
689
634
|
while IFS= read -r commit; do
|
|
690
|
-
[[ -
|
|
691
|
-
commits+=("$commit")
|
|
635
|
+
[[ -n "$commit" ]] && commits+=("$commit")
|
|
692
636
|
done < <($GIT_BIN rev-list "${base_ref}..${branch}")
|
|
693
637
|
local failed=0
|
|
694
638
|
local commit
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
function analyzeSwiftModerno({ content, functions = [], filePath, addFinding }) {
|
|
2
|
-
if (content.includes('completion:') && !content.includes('async ')) {
|
|
3
|
-
addFinding('ios.async_await_missing', 'medium', filePath, 1,
|
|
4
|
-
'Using completion handlers instead of async/await (Swift 5.9+ required)');
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
|
|
8
|
-
if (taskCount > 3 && !content.includes('TaskGroup')) {
|
|
9
|
-
addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
|
|
10
|
-
`Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (content.includes('actor ') && !content.includes(': Sendable')) {
|
|
14
|
-
addFinding('ios.sendable_missing', 'low', filePath, 1,
|
|
15
|
-
'Actor should conform to Sendable protocol for thread-safe types');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
|
|
19
|
-
addFinding('ios.opaque_types_missing', 'low', filePath, 1,
|
|
20
|
-
'Use "some View" instead of explicit View protocol return');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding') && !content.includes('@ObservedObject')) {
|
|
24
|
-
addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
|
|
25
|
-
'Consider using SwiftUI property wrappers (@State, @Binding, @ObservedObject) for state management');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const extractedFunctions = parser.extractFunctions ? parser.extractFunctions({ parsed: true }) || [] : [];
|
|
29
|
-
extractedFunctions.forEach(fn => {
|
|
30
|
-
if (fn.name.includes('Array') || fn.name.includes('Collection')) {
|
|
31
|
-
if (!content.includes('<T>') && !content.includes('<Element>')) {
|
|
32
|
-
addFinding('ios.generics_missing', 'low', filePath, fn.line,
|
|
33
|
-
`Function ${fn.name} should use generics for type safety`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding }) {
|
|
40
|
-
if (usesUIKit && !usesSwiftUI) {
|
|
41
|
-
addFinding('ios.swiftui_first', 'medium', filePath, 1,
|
|
42
|
-
'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (usesSwiftUI) {
|
|
46
|
-
if (!content.includes('@State')) {
|
|
47
|
-
addFinding('ios.state_local_missing', 'info', filePath, 1,
|
|
48
|
-
'SwiftUI view without @State - consider if local state is needed');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
|
|
52
|
-
addFinding('ios.stateobject_missing', 'high', filePath, 1,
|
|
53
|
-
'ObservableObject should be owned with @StateObject, not @ObservedObject');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
|
|
57
|
-
addFinding('ios.environmentobject_missing', 'info', filePath, 1,
|
|
58
|
-
'Consider using @EnvironmentObject for dependency injection in SwiftUI');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (content.includes('.frame(') && content.includes('CGRect(')) {
|
|
62
|
-
addFinding('ios.declarativo_missing', 'medium', filePath, 1,
|
|
63
|
-
'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
|
|
67
|
-
if (geometryReaderCount > 2) {
|
|
68
|
-
addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
|
|
69
|
-
`Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function analyzeUIKit({ classes, content, filePath, addFinding }) {
|
|
75
|
-
classes.forEach(cls => {
|
|
76
|
-
if (cls.name.includes('ViewController')) {
|
|
77
|
-
const linesCount = cls.substructure.length * 10;
|
|
78
|
-
if (linesCount > 300) {
|
|
79
|
-
addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
|
|
80
|
-
`Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!content.includes('ViewModel')) {
|
|
84
|
-
addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
|
|
85
|
-
`ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
|
|
91
|
-
if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
|
|
92
|
-
addFinding('ios.storyboards', 'high', filePath, 1,
|
|
93
|
-
'Storyboard/XIB detected - use programmatic UI for better version control');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function analyzeProtocolOriented({ protocols, content, filePath, addFinding }) {
|
|
99
|
-
if (protocols.length > 0 && !content.includes('extension ')) {
|
|
100
|
-
addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
|
|
101
|
-
'Protocols detected but no extensions - consider protocol extensions for default implementations');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (content.includes('class ') && content.includes(': ')) {
|
|
105
|
-
const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
|
|
106
|
-
if (inheritanceCount > 2) {
|
|
107
|
-
addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
|
|
108
|
-
`Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function analyzeValueTypes({ classes, content, filePath, addFinding }) {
|
|
114
|
-
classes.forEach(cls => {
|
|
115
|
-
if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
|
|
116
|
-
addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
|
|
117
|
-
`Class ${cls.name} without inheritance - consider struct for value semantics`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const varCount = (content.match(/\bvar\s+/g) || []).length;
|
|
122
|
-
const letCount = (content.match(/\blet\s+/g) || []).length;
|
|
123
|
-
if (varCount > letCount) {
|
|
124
|
-
addFinding('ios.values.mutability', 'low', filePath, 1,
|
|
125
|
-
`More var (${varCount}) than let (${letCount}) - prefer immutability`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function analyzeMemoryManagement({ content, filePath, addFinding }) {
|
|
130
|
-
const closureMatches = content.match(/\{\s*\[/g);
|
|
131
|
-
const weakSelfMatches = content.match(/\[weak self\]/g);
|
|
132
|
-
if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
|
|
133
|
-
addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
|
|
134
|
-
'Closures without [weak self] - potential retain cycles');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
|
|
138
|
-
addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
|
|
139
|
-
'Potential retain cycle - closure captures self without [weak self]');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (content.includes('class ') && !content.includes('deinit')) {
|
|
143
|
-
addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
|
|
144
|
-
'Class without deinit - consider adding for cleanup verification');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function analyzeOptionals({ content, filePath, addFinding }) {
|
|
149
|
-
const forceUnwraps = content.match(/(\w+)\s*!/g);
|
|
150
|
-
if (forceUnwraps && forceUnwraps.length > 0) {
|
|
151
|
-
const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
|
|
152
|
-
if (nonIBOutlets.length > 0) {
|
|
153
|
-
addFinding('ios.force_unwrapping', 'high', filePath, 1,
|
|
154
|
-
`Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
|
|
159
|
-
const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
|
|
160
|
-
if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
|
|
161
|
-
addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
|
|
162
|
-
'Optionals present but no optional binding - use if let or guard let');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (content.includes('?') && !content.includes('??')) {
|
|
166
|
-
addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
|
|
167
|
-
'Consider using nil coalescing operator (??) for default values');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function analyzeDependencyInjection({ classes, content, filePath, addFinding }) {
|
|
172
|
-
if (content.includes('.shared') || content.includes('static let shared')) {
|
|
173
|
-
addFinding('ios.di.singleton_usage', 'high', filePath, 1,
|
|
174
|
-
'Singleton detected - use dependency injection instead');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
classes.forEach(cls => {
|
|
178
|
-
if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
|
|
179
|
-
const hasInit = content.includes('init(');
|
|
180
|
-
if (!hasInit) {
|
|
181
|
-
addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
|
|
182
|
-
`${cls.name} should inject dependencies via initializer`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
|
|
188
|
-
addFinding('ios.di.missing_factory', 'low', filePath, 1,
|
|
189
|
-
'Complex initialization - consider factory pattern');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function analyzeNetworking({ content, filePath, addFinding }) {
|
|
194
|
-
if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (!content.includes('URLSession') && !content.includes('Alamofire')) {
|
|
198
|
-
if (content.includes('http://') || content.includes('https://')) {
|
|
199
|
-
addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
|
|
200
|
-
'Network URLs detected but no URLSession/Alamofire usage');
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
|
|
205
|
-
addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
|
|
206
|
-
'Using completion handlers with URLSession - migrate to async/await');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (content.includes('JSONSerialization') && !content.includes('Codable')) {
|
|
210
|
-
addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
|
|
211
|
-
'Manual JSON parsing - use Codable for type safety');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (content.includes('URLSession') && !content.includes('NetworkError')) {
|
|
215
|
-
addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
|
|
216
|
-
'Network code without custom NetworkError enum');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const hasSSLPinningImplementation =
|
|
220
|
-
content.includes('serverTrustPolicy') ||
|
|
221
|
-
content.includes('pinning') ||
|
|
222
|
-
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
223
|
-
|
|
224
|
-
if (content.includes('URLSession') && !hasSSLPinningImplementation) {
|
|
225
|
-
addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
|
|
226
|
-
'Consider SSL pinning for high-security apps');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (content.includes('URLSession') && !content.includes('retry')) {
|
|
230
|
-
addFinding('ios.networking.missing_retry', 'low', filePath, 1,
|
|
231
|
-
'Network requests without retry logic');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function analyzePersistence({ content, filePath, addFinding }) {
|
|
236
|
-
if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
|
|
237
|
-
addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
|
|
238
|
-
'Sensitive data in UserDefaults - use Keychain instead');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
|
|
242
|
-
addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
|
|
243
|
-
'Sensitive data detected but no Keychain usage');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (content.includes('NSManagedObjectContext') && content.includes('.main')) {
|
|
247
|
-
addFinding('ios.persistence.core_data_on_main', 'high', filePath, 1,
|
|
248
|
-
'Core Data operations on main thread - use background context');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (content.includes('NSPersistentContainer') && !content.includes('NSMigrationManager')) {
|
|
252
|
-
addFinding('ios.persistence.missing_migration', 'medium', filePath, 1,
|
|
253
|
-
'Core Data without migration strategy');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function analyzeCombine({ content, filePath, addFinding }) {
|
|
258
|
-
if (content.includes('.sink(') && !content.includes('AnyCancellable')) {
|
|
259
|
-
addFinding('ios.combine.missing_cancellables', 'high', filePath, 1,
|
|
260
|
-
'Combine sink without storing AnyCancellable - memory leak');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (content.includes('@Published') && !content.includes('import Combine')) {
|
|
264
|
-
addFinding('ios.combine.published_without_combine', 'high', filePath, 1,
|
|
265
|
-
'@Published used but Combine not imported');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (content.includes('.sink(') && !content.includes('receiveCompletion')) {
|
|
269
|
-
addFinding('ios.combine.error_handling', 'medium', filePath, 1,
|
|
270
|
-
'Combine subscriber without error handling (receiveCompletion)');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (content.includes('Future<') && !content.includes('async')) {
|
|
274
|
-
addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
|
|
275
|
-
'Combine Future for single value - consider async/await instead');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function analyzeConcurrency({ content, filePath, addFinding }) {
|
|
280
|
-
if (content.includes('DispatchQueue') && !content.includes('async func')) {
|
|
281
|
-
addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
|
|
282
|
-
'Using DispatchQueue - prefer async/await for new code');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (content.includes('DispatchQueue.main') && content.includes('UI')) {
|
|
286
|
-
addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
|
|
287
|
-
'Manual main thread dispatch - use @MainActor annotation');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
|
|
291
|
-
addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
|
|
292
|
-
'Task without cancellation handling');
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
|
|
296
|
-
addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
|
|
297
|
-
'Manual synchronization with queue - consider actor for thread safety');
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function analyzeTesting({ content, filePath, addFinding }) {
|
|
302
|
-
if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
|
|
303
|
-
addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
|
|
304
|
-
'Test file without XCTest or Quick import');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
|
|
308
|
-
addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
|
|
309
|
-
'Test without makeSUT pattern - centralize system under test creation');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
|
|
313
|
-
addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
|
|
314
|
-
'Test without trackForMemoryLeaks helper');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
|
|
318
|
-
addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
|
|
319
|
-
'Test using concrete dependencies - inject protocols for testability');
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function analyzeUITesting({ content, filePath, addFinding }) {
|
|
324
|
-
if (filePath.includes('UITest') && !content.includes('XCTest')) {
|
|
325
|
-
addFinding('ios.uitesting.missing_xctest', 'medium', filePath, 1,
|
|
326
|
-
'UI Test without XCTest import');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
|
|
330
|
-
addFinding('ios.uitesting.missing_application_launch', 'medium', filePath, 1,
|
|
331
|
-
'UI Test missing XCUIApplication launch');
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
module.exports = {
|
|
336
|
-
analyzeSwiftModerno,
|
|
337
|
-
analyzeSwiftUI,
|
|
338
|
-
analyzeUIKit,
|
|
339
|
-
analyzeProtocolOriented,
|
|
340
|
-
analyzeValueTypes,
|
|
341
|
-
analyzeMemoryManagement,
|
|
342
|
-
analyzeOptionals,
|
|
343
|
-
analyzeDependencyInjection,
|
|
344
|
-
analyzeNetworking,
|
|
345
|
-
analyzePersistence,
|
|
346
|
-
analyzeCombine,
|
|
347
|
-
analyzeConcurrency,
|
|
348
|
-
analyzeTesting,
|
|
349
|
-
analyzeUITesting,
|
|
350
|
-
};
|