semantic-complexity 0.0.7-741984fb → 0.0.15-f5745e88
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/dist/__tests__/analyzers.test.d.ts +5 -0
- package/dist/__tests__/analyzers.test.d.ts.map +1 -0
- package/dist/__tests__/analyzers.test.js +357 -0
- package/dist/__tests__/analyzers.test.js.map +1 -0
- package/dist/__tests__/gate.test.d.ts +5 -0
- package/dist/__tests__/gate.test.d.ts.map +1 -0
- package/dist/__tests__/gate.test.js +140 -0
- package/dist/__tests__/gate.test.js.map +1 -0
- package/dist/analyzers/bread.d.ts +63 -0
- package/dist/analyzers/bread.d.ts.map +1 -0
- package/dist/analyzers/bread.js +415 -0
- package/dist/analyzers/bread.js.map +1 -0
- package/dist/analyzers/cheese.d.ts +111 -0
- package/dist/analyzers/cheese.d.ts.map +1 -0
- package/dist/analyzers/cheese.js +881 -0
- package/dist/analyzers/cheese.js.map +1 -0
- package/dist/analyzers/ham.d.ts +33 -0
- package/dist/analyzers/ham.d.ts.map +1 -0
- package/dist/analyzers/ham.js +264 -0
- package/dist/analyzers/ham.js.map +1 -0
- package/dist/analyzers/index.d.ts +7 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +7 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/budget/index.d.ts +50 -0
- package/dist/budget/index.d.ts.map +1 -0
- package/dist/budget/index.js +138 -0
- package/dist/budget/index.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +103 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/gate/index.d.ts +31 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +117 -0
- package/dist/gate/index.js.map +1 -0
- package/dist/gate/waiver.d.ts +83 -0
- package/dist/gate/waiver.d.ts.map +1 -0
- package/dist/gate/waiver.js +425 -0
- package/dist/gate/waiver.js.map +1 -0
- package/dist/index.d.ts +8 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -49
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +351 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/recommend/index.d.ts +32 -0
- package/dist/recommend/index.d.ts.map +1 -0
- package/dist/recommend/index.js +169 -0
- package/dist/recommend/index.js.map +1 -0
- package/dist/simplex/index.d.ts +26 -0
- package/dist/simplex/index.d.ts.map +1 -0
- package/dist/simplex/index.js +100 -0
- package/dist/simplex/index.js.map +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +27 -52
- package/LICENSE +0 -21
- package/README.md +0 -72
- package/dist/ast/index.d.ts +0 -2
- package/dist/ast/index.d.ts.map +0 -1
- package/dist/ast/index.js +0 -2
- package/dist/ast/index.js.map +0 -1
- package/dist/ast/parser.d.ts +0 -27
- package/dist/ast/parser.d.ts.map +0 -1
- package/dist/ast/parser.js +0 -273
- package/dist/ast/parser.js.map +0 -1
- package/dist/canonical/convergence.d.ts +0 -56
- package/dist/canonical/convergence.d.ts.map +0 -1
- package/dist/canonical/convergence.js +0 -149
- package/dist/canonical/convergence.js.map +0 -1
- package/dist/canonical/index.d.ts +0 -11
- package/dist/canonical/index.d.ts.map +0 -1
- package/dist/canonical/index.js +0 -11
- package/dist/canonical/index.js.map +0 -1
- package/dist/canonical/profiles.d.ts +0 -40
- package/dist/canonical/profiles.d.ts.map +0 -1
- package/dist/canonical/profiles.js +0 -182
- package/dist/canonical/profiles.js.map +0 -1
- package/dist/canonical/types.d.ts +0 -124
- package/dist/canonical/types.d.ts.map +0 -1
- package/dist/canonical/types.js +0 -46
- package/dist/canonical/types.js.map +0 -1
- package/dist/compare.d.ts +0 -31
- package/dist/compare.d.ts.map +0 -1
- package/dist/compare.js +0 -354
- package/dist/compare.js.map +0 -1
- package/dist/context/index.d.ts +0 -41
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -253
- package/dist/context/index.js.map +0 -1
- package/dist/gates/delta.d.ts +0 -45
- package/dist/gates/delta.d.ts.map +0 -1
- package/dist/gates/delta.js +0 -260
- package/dist/gates/delta.js.map +0 -1
- package/dist/gates/index.d.ts +0 -9
- package/dist/gates/index.d.ts.map +0 -1
- package/dist/gates/index.js +0 -9
- package/dist/gates/index.js.map +0 -1
- package/dist/gates/types.d.ts +0 -96
- package/dist/gates/types.d.ts.map +0 -1
- package/dist/gates/types.js +0 -59
- package/dist/gates/types.js.map +0 -1
- package/dist/graph/call.d.ts +0 -63
- package/dist/graph/call.d.ts.map +0 -1
- package/dist/graph/call.js +0 -240
- package/dist/graph/call.js.map +0 -1
- package/dist/graph/dependency.d.ts +0 -52
- package/dist/graph/dependency.d.ts.map +0 -1
- package/dist/graph/dependency.js +0 -296
- package/dist/graph/dependency.js.map +0 -1
- package/dist/graph/index.d.ts +0 -3
- package/dist/graph/index.d.ts.map +0 -1
- package/dist/graph/index.js +0 -3
- package/dist/graph/index.js.map +0 -1
- package/dist/metrics/cognitive.d.ts +0 -42
- package/dist/metrics/cognitive.d.ts.map +0 -1
- package/dist/metrics/cognitive.js +0 -204
- package/dist/metrics/cognitive.js.map +0 -1
- package/dist/metrics/cyclomatic.d.ts +0 -31
- package/dist/metrics/cyclomatic.d.ts.map +0 -1
- package/dist/metrics/cyclomatic.js +0 -121
- package/dist/metrics/cyclomatic.js.map +0 -1
- package/dist/metrics/dimensional.d.ts +0 -32
- package/dist/metrics/dimensional.d.ts.map +0 -1
- package/dist/metrics/dimensional.js +0 -560
- package/dist/metrics/dimensional.js.map +0 -1
- package/dist/metrics/index.d.ts +0 -26
- package/dist/metrics/index.d.ts.map +0 -1
- package/dist/metrics/index.js +0 -120
- package/dist/metrics/index.js.map +0 -1
- package/dist/metrics/meta.d.ts +0 -90
- package/dist/metrics/meta.d.ts.map +0 -1
- package/dist/metrics/meta.js +0 -144
- package/dist/metrics/meta.js.map +0 -1
- package/dist/tensor/canonical.d.ts +0 -53
- package/dist/tensor/canonical.d.ts.map +0 -1
- package/dist/tensor/canonical.js +0 -192
- package/dist/tensor/canonical.js.map +0 -1
- package/dist/tensor/index.d.ts +0 -22
- package/dist/tensor/index.d.ts.map +0 -1
- package/dist/tensor/index.js +0 -22
- package/dist/tensor/index.js.map +0 -1
- package/dist/tensor/matrix.d.ts +0 -62
- package/dist/tensor/matrix.d.ts.map +0 -1
- package/dist/tensor/matrix.js +0 -187
- package/dist/tensor/matrix.js.map +0 -1
- package/dist/tensor/scoring.d.ts +0 -82
- package/dist/tensor/scoring.d.ts.map +0 -1
- package/dist/tensor/scoring.js +0 -233
- package/dist/tensor/scoring.js.map +0 -1
- package/dist/tensor/types.d.ts +0 -186
- package/dist/tensor/types.d.ts.map +0 -1
- package/dist/tensor/types.js +0 -15
- package/dist/tensor/types.js.map +0 -1
- package/dist/types.d.ts +0 -341
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -22
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🧀 Cheese Analyzer - Cognitive Accessibility (TypeScript)
|
|
3
|
+
*
|
|
4
|
+
* 인지 가능 조건 (4가지 모두 충족):
|
|
5
|
+
* 1. 중첩 깊이 ≤ 4
|
|
6
|
+
* 2. 개념 수 ≤ 9/함수 (Miller's Law: 7±2)
|
|
7
|
+
* 3. 숨겨진 의존성 ≤ 2
|
|
8
|
+
* 4. state×async×retry 2개 이상 공존 금지
|
|
9
|
+
*
|
|
10
|
+
* TypeScript 전용 복잡도 분석:
|
|
11
|
+
* - 제네릭 복잡도 (타입 파라미터 수, constraints, nested)
|
|
12
|
+
* - 유니온/인터섹션 타입 복잡도
|
|
13
|
+
* - 조건부 타입 복잡도
|
|
14
|
+
* - 매핑 타입 복잡도
|
|
15
|
+
* - 타입 가드 복잡도
|
|
16
|
+
* - 데코레이터 스택 복잡도
|
|
17
|
+
*
|
|
18
|
+
* Anti-pattern Penalty (TypeScript):
|
|
19
|
+
* - Rest parameters (...args) → +3 개념
|
|
20
|
+
* - Object spread for config bundling → +3 개념
|
|
21
|
+
* - Complex generics (>3 params or nested) → +2 per level
|
|
22
|
+
* - Deep union types (>4 members) → +1 per extra
|
|
23
|
+
* - Conditional type chains → +2 per level
|
|
24
|
+
*/
|
|
25
|
+
import ts from 'typescript';
|
|
26
|
+
// =============================================================
|
|
27
|
+
// Essential Complexity Waiver
|
|
28
|
+
// =============================================================
|
|
29
|
+
// @ts-ignore: Waiver declaration for gate system - parsed at runtime
|
|
30
|
+
const __essential_complexity__ = {
|
|
31
|
+
adr: '../../docs/adr/001-ast-analyzer-complexity.md',
|
|
32
|
+
nesting: 7,
|
|
33
|
+
concepts: { total: 26 },
|
|
34
|
+
reason: 'AST Visitor 패턴 - 본질적 복잡도',
|
|
35
|
+
};
|
|
36
|
+
const FRAMEWORK_CONFIGS = {
|
|
37
|
+
react: {
|
|
38
|
+
name: 'react',
|
|
39
|
+
jsxNestingWeight: 0.3,
|
|
40
|
+
hookConceptWeight: 0.5,
|
|
41
|
+
chainMethodWeight: 0.5,
|
|
42
|
+
propsDestructureWeight: 0.3,
|
|
43
|
+
},
|
|
44
|
+
vue: {
|
|
45
|
+
name: 'vue',
|
|
46
|
+
jsxNestingWeight: 0.3,
|
|
47
|
+
hookConceptWeight: 0.5, // Composition API
|
|
48
|
+
chainMethodWeight: 0.5,
|
|
49
|
+
propsDestructureWeight: 0.3,
|
|
50
|
+
},
|
|
51
|
+
angular: {
|
|
52
|
+
name: 'angular',
|
|
53
|
+
jsxNestingWeight: 0.4,
|
|
54
|
+
hookConceptWeight: 0.7,
|
|
55
|
+
chainMethodWeight: 0.5,
|
|
56
|
+
propsDestructureWeight: 0.5,
|
|
57
|
+
},
|
|
58
|
+
svelte: {
|
|
59
|
+
name: 'svelte',
|
|
60
|
+
jsxNestingWeight: 0.3,
|
|
61
|
+
hookConceptWeight: 0.5,
|
|
62
|
+
chainMethodWeight: 0.5,
|
|
63
|
+
propsDestructureWeight: 0.3,
|
|
64
|
+
},
|
|
65
|
+
none: {
|
|
66
|
+
name: 'none',
|
|
67
|
+
jsxNestingWeight: 1.0,
|
|
68
|
+
hookConceptWeight: 1.0,
|
|
69
|
+
chainMethodWeight: 1.0,
|
|
70
|
+
propsDestructureWeight: 1.0,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
// Hook/Composition API 패턴
|
|
74
|
+
const HOOK_PATTERNS = {
|
|
75
|
+
react: [
|
|
76
|
+
/\buse[A-Z]\w*\s*\(/, // useState, useEffect, useCustomHook
|
|
77
|
+
/\buseState\b/,
|
|
78
|
+
/\buseEffect\b/,
|
|
79
|
+
/\buseCallback\b/,
|
|
80
|
+
/\buseMemo\b/,
|
|
81
|
+
/\buseRef\b/,
|
|
82
|
+
/\buseContext\b/,
|
|
83
|
+
/\buseReducer\b/,
|
|
84
|
+
],
|
|
85
|
+
vue: [
|
|
86
|
+
/\bref\s*\(/,
|
|
87
|
+
/\breactive\s*\(/,
|
|
88
|
+
/\bcomputed\s*\(/,
|
|
89
|
+
/\bwatch\s*\(/,
|
|
90
|
+
/\bwatchEffect\s*\(/,
|
|
91
|
+
/\bonMounted\s*\(/,
|
|
92
|
+
/\bonUnmounted\s*\(/,
|
|
93
|
+
],
|
|
94
|
+
angular: [
|
|
95
|
+
/\b@Input\s*\(/,
|
|
96
|
+
/\b@Output\s*\(/,
|
|
97
|
+
/\b@ViewChild\s*\(/,
|
|
98
|
+
/\bngOnInit\b/,
|
|
99
|
+
],
|
|
100
|
+
svelte: [
|
|
101
|
+
/\$:\s*/, // reactive statements
|
|
102
|
+
/\bonMount\s*\(/,
|
|
103
|
+
/\bonDestroy\s*\(/,
|
|
104
|
+
],
|
|
105
|
+
none: [],
|
|
106
|
+
};
|
|
107
|
+
// 체이닝 메서드 패턴
|
|
108
|
+
const CHAIN_METHOD_PATTERNS = [
|
|
109
|
+
/\.map\s*\(/,
|
|
110
|
+
/\.filter\s*\(/,
|
|
111
|
+
/\.reduce\s*\(/,
|
|
112
|
+
/\.find\s*\(/,
|
|
113
|
+
/\.findIndex\s*\(/,
|
|
114
|
+
/\.some\s*\(/,
|
|
115
|
+
/\.every\s*\(/,
|
|
116
|
+
/\.flatMap\s*\(/,
|
|
117
|
+
/\.forEach\s*\(/,
|
|
118
|
+
];
|
|
119
|
+
// =============================================================
|
|
120
|
+
// Default Config
|
|
121
|
+
// =============================================================
|
|
122
|
+
const DEFAULT_CONFIG = {
|
|
123
|
+
nestingThreshold: 4,
|
|
124
|
+
conceptsPerFunction: 9, // Miller's Law: 7±2
|
|
125
|
+
hiddenDepThreshold: 2,
|
|
126
|
+
};
|
|
127
|
+
// Anti-pattern penalties
|
|
128
|
+
const REST_PARAMS_PENALTY = 3;
|
|
129
|
+
const SPREAD_CONFIG_PENALTY = 3;
|
|
130
|
+
// TypeScript 타입 시스템 페널티
|
|
131
|
+
const GENERIC_PARAMS_THRESHOLD = 3; // 3개 초과 시 페널티
|
|
132
|
+
const GENERIC_PARAMS_PENALTY = 2; // 초과당 +2
|
|
133
|
+
const GENERIC_DEPTH_THRESHOLD = 2; // 중첩 2 초과 시 페널티
|
|
134
|
+
const GENERIC_DEPTH_PENALTY = 2; // 레벨당 +2
|
|
135
|
+
const GENERIC_CONSTRAINT_PENALTY = 1; // constraint당 +1
|
|
136
|
+
const UNION_MEMBERS_THRESHOLD = 4; // 4개 초과 시 페널티
|
|
137
|
+
const UNION_MEMBERS_PENALTY = 1; // 초과당 +1
|
|
138
|
+
const INTERSECTION_PENALTY = 1; // 인터섹션당 +1
|
|
139
|
+
const CONDITIONAL_TYPE_PENALTY = 2; // 조건부 타입당 +2
|
|
140
|
+
const MAPPED_TYPE_PENALTY = 2; // 매핑 타입당 +2
|
|
141
|
+
const TYPE_GUARD_PENALTY = 1; // 타입 가드당 +1
|
|
142
|
+
const DECORATOR_STACK_THRESHOLD = 3; // 3개 이상 연속 시 페널티
|
|
143
|
+
const DECORATOR_STACK_PENALTY = 2; // 스택당 +2
|
|
144
|
+
// Built-in functions (low cognitive load)
|
|
145
|
+
const BUILTIN_FUNCTIONS = new Set([
|
|
146
|
+
'parseInt', 'parseFloat', 'String', 'Number', 'Boolean',
|
|
147
|
+
'Array', 'Object', 'JSON', 'Math', 'Date', 'Promise',
|
|
148
|
+
'console', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
149
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
|
150
|
+
]);
|
|
151
|
+
// =============================================================
|
|
152
|
+
// Main Analyzer
|
|
153
|
+
// =============================================================
|
|
154
|
+
export function analyzeCheese(source, config = DEFAULT_CONFIG) {
|
|
155
|
+
const sourceFile = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.Latest, true);
|
|
156
|
+
// 프레임워크 감지 및 보정
|
|
157
|
+
const frameworkType = config.framework || detectFramework(source);
|
|
158
|
+
const frameworkConfig = FRAMEWORK_CONFIGS[frameworkType];
|
|
159
|
+
const frameworkInfo = analyzeFrameworkPatterns(sourceFile, source, frameworkConfig);
|
|
160
|
+
// 중첩 분석 (JSX와 로직 분리)
|
|
161
|
+
const { maxNesting, jsxNesting, logicNesting } = calculateNestingWithFramework(sourceFile);
|
|
162
|
+
frameworkInfo.jsxNestingDepth = jsxNesting;
|
|
163
|
+
frameworkInfo.logicNestingDepth = logicNesting;
|
|
164
|
+
// 보정된 중첩 계산
|
|
165
|
+
const adjustedNesting = calculateAdjustedNesting(jsxNesting, logicNesting, frameworkConfig, frameworkInfo.adjustments);
|
|
166
|
+
const functions = analyzeFunctionsWithFramework(sourceFile, source, frameworkConfig, frameworkInfo);
|
|
167
|
+
const hiddenDeps = countHiddenDependencies(sourceFile);
|
|
168
|
+
const stateAsyncRetry = checkStateAsyncRetry(source, sourceFile);
|
|
169
|
+
// TypeScript 전용 복잡도 분석
|
|
170
|
+
const typeComplexity = analyzeTypeComplexity(sourceFile);
|
|
171
|
+
const violations = collectViolations(adjustedNesting, // 보정된 값 사용
|
|
172
|
+
functions, hiddenDeps, stateAsyncRetry, typeComplexity, config);
|
|
173
|
+
return {
|
|
174
|
+
accessible: violations.length === 0,
|
|
175
|
+
reason: violations.length === 0 ? 'All conditions met' : violations[0],
|
|
176
|
+
violations,
|
|
177
|
+
maxNesting,
|
|
178
|
+
adjustedNesting,
|
|
179
|
+
functions,
|
|
180
|
+
hiddenDependencies: hiddenDeps,
|
|
181
|
+
stateAsyncRetry,
|
|
182
|
+
config,
|
|
183
|
+
typeComplexity,
|
|
184
|
+
frameworkInfo,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// =============================================================
|
|
188
|
+
// Helper Functions
|
|
189
|
+
// =============================================================
|
|
190
|
+
function getFunctionName(node) {
|
|
191
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
192
|
+
return node.name?.getText() || '<anonymous>';
|
|
193
|
+
}
|
|
194
|
+
// Arrow function: check parent for variable name
|
|
195
|
+
const parent = node.parent;
|
|
196
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
197
|
+
return parent.name.text;
|
|
198
|
+
}
|
|
199
|
+
if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) {
|
|
200
|
+
return parent.name.text;
|
|
201
|
+
}
|
|
202
|
+
return '<anonymous>';
|
|
203
|
+
}
|
|
204
|
+
function getCallName(node, sourceFile) {
|
|
205
|
+
const expr = node.expression;
|
|
206
|
+
if (ts.isIdentifier(expr)) {
|
|
207
|
+
return expr.text;
|
|
208
|
+
}
|
|
209
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
210
|
+
return expr.getText(sourceFile);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// =============================================================
|
|
215
|
+
// Condition 3: Hidden Dependencies
|
|
216
|
+
// =============================================================
|
|
217
|
+
function countHiddenDependencies(sourceFile) {
|
|
218
|
+
let count = 0;
|
|
219
|
+
function visit(node) {
|
|
220
|
+
// Global access
|
|
221
|
+
if (ts.isIdentifier(node)) {
|
|
222
|
+
const name = node.text;
|
|
223
|
+
if (['global', 'globalThis', 'window', 'document'].includes(name)) {
|
|
224
|
+
count++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// process.env access
|
|
228
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
229
|
+
const text = node.getText(sourceFile);
|
|
230
|
+
if (text.startsWith('process.env') || text.startsWith('import.meta.env')) {
|
|
231
|
+
count++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
ts.forEachChild(node, visit);
|
|
235
|
+
}
|
|
236
|
+
visit(sourceFile);
|
|
237
|
+
return count;
|
|
238
|
+
}
|
|
239
|
+
// =============================================================
|
|
240
|
+
// Condition 4: State × Async × Retry
|
|
241
|
+
// =============================================================
|
|
242
|
+
function checkStateAsyncRetry(source, sourceFile) {
|
|
243
|
+
// State detection
|
|
244
|
+
const hasState = detectState(source, sourceFile);
|
|
245
|
+
// Async detection
|
|
246
|
+
const hasAsync = detectAsync(source, sourceFile);
|
|
247
|
+
// Retry detection
|
|
248
|
+
const hasRetry = detectRetry(source);
|
|
249
|
+
const axes = [];
|
|
250
|
+
if (hasState)
|
|
251
|
+
axes.push('state');
|
|
252
|
+
if (hasAsync)
|
|
253
|
+
axes.push('async');
|
|
254
|
+
if (hasRetry)
|
|
255
|
+
axes.push('retry');
|
|
256
|
+
return {
|
|
257
|
+
hasState,
|
|
258
|
+
hasAsync,
|
|
259
|
+
hasRetry,
|
|
260
|
+
axes,
|
|
261
|
+
count: axes.length,
|
|
262
|
+
violated: axes.length >= 2,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function detectState(source, sourceFile) {
|
|
266
|
+
// Class field mutation (this.field = ...)
|
|
267
|
+
if (/this\.\w+\s*=/.test(source)) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
// React state
|
|
271
|
+
if (/useState|useReducer|setState/.test(source)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
// Zustand/Redux/MobX
|
|
275
|
+
if (/createStore|makeAutoObservable|observable/.test(source)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// Let variable with reassignment
|
|
279
|
+
let hasLetReassignment = false;
|
|
280
|
+
function visit(node) {
|
|
281
|
+
if (ts.isVariableDeclaration(node)) {
|
|
282
|
+
const parent = node.parent;
|
|
283
|
+
if (ts.isVariableDeclarationList(parent) && parent.flags & ts.NodeFlags.Let) {
|
|
284
|
+
// Check if reassigned later (simple heuristic)
|
|
285
|
+
const varName = node.name.getText(sourceFile);
|
|
286
|
+
if (new RegExp(`${varName}\\s*=(?!=)`).test(source)) {
|
|
287
|
+
hasLetReassignment = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
ts.forEachChild(node, visit);
|
|
292
|
+
}
|
|
293
|
+
visit(sourceFile);
|
|
294
|
+
return hasLetReassignment;
|
|
295
|
+
}
|
|
296
|
+
function detectAsync(source, _sourceFile) {
|
|
297
|
+
// Explicit async/await
|
|
298
|
+
if (/async\s+(function|\()|\bawait\b/.test(source)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
// Promise usage
|
|
302
|
+
if (/new\s+Promise|\.then\s*\(|\.catch\s*\(/.test(source)) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
// Worker/concurrent
|
|
306
|
+
if (/Worker|spawn|fork/.test(source)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
function detectRetry(source) {
|
|
312
|
+
// Retry decorators/libraries
|
|
313
|
+
const retryPatterns = [
|
|
314
|
+
/@retry/i,
|
|
315
|
+
/retry\s*\(/i,
|
|
316
|
+
/p-retry|async-retry/i,
|
|
317
|
+
/exponentialBackoff/i,
|
|
318
|
+
/retryable/i,
|
|
319
|
+
];
|
|
320
|
+
if (retryPatterns.some(p => p.test(source))) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
// Manual retry pattern: for/while + try/catch + continue/break
|
|
324
|
+
if (/for\s*\([^)]*retry|while\s*\([^)]*retry/i.test(source)) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
// loop with try-catch and counter
|
|
328
|
+
if (/for\s*\(.*attempts?|while\s*\(.*attempts?/i.test(source)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
// =============================================================
|
|
334
|
+
// Collect Violations
|
|
335
|
+
// =============================================================
|
|
336
|
+
function collectViolations(maxNesting, functions, hiddenDeps, sar, typeComplexity, config) {
|
|
337
|
+
const violations = [];
|
|
338
|
+
// 1. Nesting
|
|
339
|
+
if (maxNesting > config.nestingThreshold) {
|
|
340
|
+
violations.push(`nesting depth ${maxNesting} > ${config.nestingThreshold}`);
|
|
341
|
+
}
|
|
342
|
+
// 2. Concepts per function (타입 복잡도 페널티 포함)
|
|
343
|
+
const typePenaltyPerFunction = Math.floor(typeComplexity.totalPenalty / Math.max(functions.length, 1));
|
|
344
|
+
for (const fn of functions) {
|
|
345
|
+
const totalConcepts = fn.conceptCount + typePenaltyPerFunction;
|
|
346
|
+
if (totalConcepts > config.conceptsPerFunction) {
|
|
347
|
+
violations.push(`${fn.name}: ${totalConcepts} concepts > ${config.conceptsPerFunction} (type penalty: ${typePenaltyPerFunction})`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// 3. Hidden dependencies
|
|
351
|
+
if (hiddenDeps > config.hiddenDepThreshold) {
|
|
352
|
+
violations.push(`hidden deps ${hiddenDeps} > ${config.hiddenDepThreshold}`);
|
|
353
|
+
}
|
|
354
|
+
// 4. State × Async × Retry
|
|
355
|
+
if (sar.violated) {
|
|
356
|
+
violations.push(`state×async×retry: ${sar.axes.join('×')} (${sar.count} >= 2)`);
|
|
357
|
+
}
|
|
358
|
+
// 5. TypeScript 타입 복잡도 위반
|
|
359
|
+
if (typeComplexity.generics.maxParams > GENERIC_PARAMS_THRESHOLD + 2) {
|
|
360
|
+
violations.push(`excessive generic params: ${typeComplexity.generics.maxParams} > ${GENERIC_PARAMS_THRESHOLD + 2}`);
|
|
361
|
+
}
|
|
362
|
+
if (typeComplexity.generics.maxDepth > GENERIC_DEPTH_THRESHOLD + 1) {
|
|
363
|
+
violations.push(`deeply nested generics: depth ${typeComplexity.generics.maxDepth}`);
|
|
364
|
+
}
|
|
365
|
+
if (typeComplexity.unions.maxMembers > UNION_MEMBERS_THRESHOLD + 4) {
|
|
366
|
+
violations.push(`excessive union members: ${typeComplexity.unions.maxMembers} > ${UNION_MEMBERS_THRESHOLD + 4}`);
|
|
367
|
+
}
|
|
368
|
+
if (typeComplexity.conditionalTypes > 3) {
|
|
369
|
+
violations.push(`too many conditional types: ${typeComplexity.conditionalTypes}`);
|
|
370
|
+
}
|
|
371
|
+
if (typeComplexity.decoratorStacks > 0) {
|
|
372
|
+
violations.push(`decorator stacks detected: ${typeComplexity.decoratorStacks}`);
|
|
373
|
+
}
|
|
374
|
+
return violations;
|
|
375
|
+
}
|
|
376
|
+
// =============================================================
|
|
377
|
+
// TypeScript Type Complexity Analysis
|
|
378
|
+
// =============================================================
|
|
379
|
+
function analyzeTypeComplexity(sourceFile) {
|
|
380
|
+
const generics = analyzeGenericComplexity(sourceFile);
|
|
381
|
+
const unions = analyzeUnionComplexity(sourceFile);
|
|
382
|
+
const conditionalTypes = countConditionalTypes(sourceFile);
|
|
383
|
+
const mappedTypes = countMappedTypes(sourceFile);
|
|
384
|
+
const typeGuards = countTypeGuards(sourceFile);
|
|
385
|
+
const decoratorStacks = countDecoratorStacks(sourceFile);
|
|
386
|
+
const totalPenalty = generics.penalty +
|
|
387
|
+
unions.penalty +
|
|
388
|
+
conditionalTypes * CONDITIONAL_TYPE_PENALTY +
|
|
389
|
+
mappedTypes * MAPPED_TYPE_PENALTY +
|
|
390
|
+
typeGuards * TYPE_GUARD_PENALTY +
|
|
391
|
+
decoratorStacks * DECORATOR_STACK_PENALTY;
|
|
392
|
+
return {
|
|
393
|
+
generics,
|
|
394
|
+
unions,
|
|
395
|
+
conditionalTypes,
|
|
396
|
+
mappedTypes,
|
|
397
|
+
typeGuards,
|
|
398
|
+
decoratorStacks,
|
|
399
|
+
totalPenalty,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 타입 파라미터 정보 추출 (개념 수 ~5)
|
|
404
|
+
*/
|
|
405
|
+
function extractTypeParameterInfo(typeParameters, typeNode) {
|
|
406
|
+
if (!typeParameters)
|
|
407
|
+
return { paramCount: 0, constrainedCount: 0, depth: 0 };
|
|
408
|
+
const paramCount = typeParameters.length;
|
|
409
|
+
const constrainedCount = typeParameters.filter(p => p.constraint).length;
|
|
410
|
+
const depth = typeNode ? getGenericDepth(typeNode) : 0;
|
|
411
|
+
return { paramCount, constrainedCount, depth };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* 제네릭 복잡도 분석 (개념 수 ~8)
|
|
415
|
+
*/
|
|
416
|
+
function analyzeGenericComplexity(sourceFile) {
|
|
417
|
+
let count = 0;
|
|
418
|
+
let maxParams = 0;
|
|
419
|
+
let maxDepth = 0;
|
|
420
|
+
let constrainedCount = 0;
|
|
421
|
+
function processGenericNode(info) {
|
|
422
|
+
count++;
|
|
423
|
+
if (info.paramCount > maxParams)
|
|
424
|
+
maxParams = info.paramCount;
|
|
425
|
+
if (info.depth > maxDepth)
|
|
426
|
+
maxDepth = info.depth;
|
|
427
|
+
constrainedCount += info.constrainedCount;
|
|
428
|
+
}
|
|
429
|
+
function visit(node) {
|
|
430
|
+
if (ts.isTypeAliasDeclaration(node) && node.typeParameters) {
|
|
431
|
+
processGenericNode(extractTypeParameterInfo(node.typeParameters, node.type));
|
|
432
|
+
}
|
|
433
|
+
if (ts.isInterfaceDeclaration(node) && node.typeParameters) {
|
|
434
|
+
processGenericNode(extractTypeParameterInfo(node.typeParameters));
|
|
435
|
+
}
|
|
436
|
+
if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) && node.typeParameters) {
|
|
437
|
+
processGenericNode(extractTypeParameterInfo(node.typeParameters));
|
|
438
|
+
}
|
|
439
|
+
if (ts.isClassDeclaration(node) && node.typeParameters) {
|
|
440
|
+
processGenericNode(extractTypeParameterInfo(node.typeParameters));
|
|
441
|
+
}
|
|
442
|
+
ts.forEachChild(node, visit);
|
|
443
|
+
}
|
|
444
|
+
visit(sourceFile);
|
|
445
|
+
const penalty = calculateGenericPenalty(maxParams, maxDepth, constrainedCount);
|
|
446
|
+
return { count, maxParams, maxDepth, constrainedCount, penalty };
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 제네릭 페널티 계산 (개념 수 ~4)
|
|
450
|
+
*/
|
|
451
|
+
function calculateGenericPenalty(maxParams, maxDepth, constrainedCount) {
|
|
452
|
+
let penalty = 0;
|
|
453
|
+
if (maxParams > GENERIC_PARAMS_THRESHOLD) {
|
|
454
|
+
penalty += (maxParams - GENERIC_PARAMS_THRESHOLD) * GENERIC_PARAMS_PENALTY;
|
|
455
|
+
}
|
|
456
|
+
if (maxDepth > GENERIC_DEPTH_THRESHOLD) {
|
|
457
|
+
penalty += (maxDepth - GENERIC_DEPTH_THRESHOLD) * GENERIC_DEPTH_PENALTY;
|
|
458
|
+
}
|
|
459
|
+
penalty += constrainedCount * GENERIC_CONSTRAINT_PENALTY;
|
|
460
|
+
return penalty;
|
|
461
|
+
}
|
|
462
|
+
function getGenericDepth(node) {
|
|
463
|
+
let maxDepth = 0;
|
|
464
|
+
function visit(n, depth) {
|
|
465
|
+
if (ts.isTypeReferenceNode(n) && n.typeArguments) {
|
|
466
|
+
depth++;
|
|
467
|
+
if (depth > maxDepth)
|
|
468
|
+
maxDepth = depth;
|
|
469
|
+
n.typeArguments.forEach(arg => visit(arg, depth));
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
ts.forEachChild(n, child => visit(child, depth));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
visit(node, 0);
|
|
476
|
+
return maxDepth;
|
|
477
|
+
}
|
|
478
|
+
function analyzeUnionComplexity(sourceFile) {
|
|
479
|
+
let count = 0;
|
|
480
|
+
let maxMembers = 0;
|
|
481
|
+
let intersectionCount = 0;
|
|
482
|
+
function visit(node) {
|
|
483
|
+
// Union type: A | B | C
|
|
484
|
+
if (ts.isUnionTypeNode(node)) {
|
|
485
|
+
count++;
|
|
486
|
+
const members = node.types.length;
|
|
487
|
+
if (members > maxMembers)
|
|
488
|
+
maxMembers = members;
|
|
489
|
+
}
|
|
490
|
+
// Intersection type: A & B & C
|
|
491
|
+
if (ts.isIntersectionTypeNode(node)) {
|
|
492
|
+
intersectionCount++;
|
|
493
|
+
}
|
|
494
|
+
ts.forEachChild(node, visit);
|
|
495
|
+
}
|
|
496
|
+
visit(sourceFile);
|
|
497
|
+
// Calculate penalty
|
|
498
|
+
let penalty = 0;
|
|
499
|
+
if (maxMembers > UNION_MEMBERS_THRESHOLD) {
|
|
500
|
+
penalty += (maxMembers - UNION_MEMBERS_THRESHOLD) * UNION_MEMBERS_PENALTY;
|
|
501
|
+
}
|
|
502
|
+
penalty += intersectionCount * INTERSECTION_PENALTY;
|
|
503
|
+
return { count, maxMembers, intersectionCount, penalty };
|
|
504
|
+
}
|
|
505
|
+
function countConditionalTypes(sourceFile) {
|
|
506
|
+
let count = 0;
|
|
507
|
+
function visit(node) {
|
|
508
|
+
// Conditional type: T extends U ? X : Y
|
|
509
|
+
if (ts.isConditionalTypeNode(node)) {
|
|
510
|
+
count++;
|
|
511
|
+
}
|
|
512
|
+
ts.forEachChild(node, visit);
|
|
513
|
+
}
|
|
514
|
+
visit(sourceFile);
|
|
515
|
+
return count;
|
|
516
|
+
}
|
|
517
|
+
function countMappedTypes(sourceFile) {
|
|
518
|
+
let count = 0;
|
|
519
|
+
function visit(node) {
|
|
520
|
+
// Mapped type: { [K in keyof T]: ... }
|
|
521
|
+
if (ts.isMappedTypeNode(node)) {
|
|
522
|
+
count++;
|
|
523
|
+
}
|
|
524
|
+
ts.forEachChild(node, visit);
|
|
525
|
+
}
|
|
526
|
+
visit(sourceFile);
|
|
527
|
+
return count;
|
|
528
|
+
}
|
|
529
|
+
function countTypeGuards(sourceFile) {
|
|
530
|
+
let count = 0;
|
|
531
|
+
function visit(node) {
|
|
532
|
+
// Type predicate: x is Foo
|
|
533
|
+
if (ts.isTypePredicateNode(node)) {
|
|
534
|
+
count++;
|
|
535
|
+
}
|
|
536
|
+
// Function with type predicate return type
|
|
537
|
+
if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) &&
|
|
538
|
+
node.type &&
|
|
539
|
+
ts.isTypePredicateNode(node.type)) {
|
|
540
|
+
count++;
|
|
541
|
+
}
|
|
542
|
+
ts.forEachChild(node, visit);
|
|
543
|
+
}
|
|
544
|
+
visit(sourceFile);
|
|
545
|
+
return count;
|
|
546
|
+
}
|
|
547
|
+
function countDecoratorStacks(sourceFile) {
|
|
548
|
+
let stacks = 0;
|
|
549
|
+
function visit(node) {
|
|
550
|
+
// Check for decorator stacks on classes, methods, properties
|
|
551
|
+
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
|
|
552
|
+
if (decorators && decorators.length >= DECORATOR_STACK_THRESHOLD) {
|
|
553
|
+
stacks++;
|
|
554
|
+
}
|
|
555
|
+
ts.forEachChild(node, visit);
|
|
556
|
+
}
|
|
557
|
+
visit(sourceFile);
|
|
558
|
+
return stacks;
|
|
559
|
+
}
|
|
560
|
+
// =============================================================
|
|
561
|
+
// Framework Detection & Adjustment
|
|
562
|
+
// =============================================================
|
|
563
|
+
function detectFramework(source) {
|
|
564
|
+
// React 감지
|
|
565
|
+
if (/from\s+['"]react['"]/.test(source) ||
|
|
566
|
+
/import\s+React/.test(source) ||
|
|
567
|
+
/['"]react['"]/.test(source)) {
|
|
568
|
+
return 'react';
|
|
569
|
+
}
|
|
570
|
+
// Vue 감지
|
|
571
|
+
if (/from\s+['"]vue['"]/.test(source) ||
|
|
572
|
+
/defineComponent|ref\(|reactive\(/.test(source)) {
|
|
573
|
+
return 'vue';
|
|
574
|
+
}
|
|
575
|
+
// Angular 감지
|
|
576
|
+
if (/@Component\s*\(|@Injectable\s*\(|@NgModule/.test(source) ||
|
|
577
|
+
/from\s+['"]@angular/.test(source)) {
|
|
578
|
+
return 'angular';
|
|
579
|
+
}
|
|
580
|
+
// Svelte 감지
|
|
581
|
+
if (/from\s+['"]svelte['"]/.test(source) ||
|
|
582
|
+
/\$:\s*/.test(source)) {
|
|
583
|
+
return 'svelte';
|
|
584
|
+
}
|
|
585
|
+
return 'none';
|
|
586
|
+
}
|
|
587
|
+
function analyzeFrameworkPatterns(_sourceFile, source, config) {
|
|
588
|
+
const hookPatterns = HOOK_PATTERNS[config.name];
|
|
589
|
+
let hookCount = 0;
|
|
590
|
+
let chainCount = 0;
|
|
591
|
+
// Hook 패턴 카운트
|
|
592
|
+
for (const pattern of hookPatterns) {
|
|
593
|
+
const matches = source.match(new RegExp(pattern.source, 'g'));
|
|
594
|
+
if (matches) {
|
|
595
|
+
hookCount += matches.length;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// 체이닝 메서드 카운트
|
|
599
|
+
for (const pattern of CHAIN_METHOD_PATTERNS) {
|
|
600
|
+
const matches = source.match(new RegExp(pattern.source, 'g'));
|
|
601
|
+
if (matches) {
|
|
602
|
+
chainCount += matches.length;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
detected: config.name,
|
|
607
|
+
config,
|
|
608
|
+
jsxNestingDepth: 0, // 나중에 계산
|
|
609
|
+
logicNestingDepth: 0, // 나중에 계산
|
|
610
|
+
hookCount,
|
|
611
|
+
chainCount,
|
|
612
|
+
adjustments: [],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function calculateNestingWithFramework(sourceFile) {
|
|
616
|
+
let maxNesting = 0;
|
|
617
|
+
let maxJsxNesting = 0;
|
|
618
|
+
let maxLogicNesting = 0;
|
|
619
|
+
// JSX 요소 노드 종류
|
|
620
|
+
const jsxNodeKinds = new Set([
|
|
621
|
+
ts.SyntaxKind.JsxElement,
|
|
622
|
+
ts.SyntaxKind.JsxSelfClosingElement,
|
|
623
|
+
ts.SyntaxKind.JsxFragment,
|
|
624
|
+
ts.SyntaxKind.JsxOpeningElement,
|
|
625
|
+
]);
|
|
626
|
+
// 로직 중첩 노드 종류
|
|
627
|
+
const logicNodeKinds = new Set([
|
|
628
|
+
ts.SyntaxKind.IfStatement,
|
|
629
|
+
ts.SyntaxKind.ForStatement,
|
|
630
|
+
ts.SyntaxKind.ForInStatement,
|
|
631
|
+
ts.SyntaxKind.ForOfStatement,
|
|
632
|
+
ts.SyntaxKind.WhileStatement,
|
|
633
|
+
ts.SyntaxKind.DoStatement,
|
|
634
|
+
ts.SyntaxKind.TryStatement,
|
|
635
|
+
ts.SyntaxKind.CatchClause,
|
|
636
|
+
ts.SyntaxKind.SwitchStatement,
|
|
637
|
+
ts.SyntaxKind.ConditionalExpression,
|
|
638
|
+
]);
|
|
639
|
+
function visit(node, totalDepth, jsxDepth, logicDepth) {
|
|
640
|
+
let newTotalDepth = totalDepth;
|
|
641
|
+
let newJsxDepth = jsxDepth;
|
|
642
|
+
let newLogicDepth = logicDepth;
|
|
643
|
+
if (jsxNodeKinds.has(node.kind)) {
|
|
644
|
+
newTotalDepth++;
|
|
645
|
+
newJsxDepth++;
|
|
646
|
+
if (newJsxDepth > maxJsxNesting)
|
|
647
|
+
maxJsxNesting = newJsxDepth;
|
|
648
|
+
}
|
|
649
|
+
else if (logicNodeKinds.has(node.kind)) {
|
|
650
|
+
newTotalDepth++;
|
|
651
|
+
newLogicDepth++;
|
|
652
|
+
if (newLogicDepth > maxLogicNesting)
|
|
653
|
+
maxLogicNesting = newLogicDepth;
|
|
654
|
+
}
|
|
655
|
+
else if (node.kind === ts.SyntaxKind.ArrowFunction ||
|
|
656
|
+
node.kind === ts.SyntaxKind.FunctionExpression ||
|
|
657
|
+
node.kind === ts.SyntaxKind.FunctionDeclaration ||
|
|
658
|
+
node.kind === ts.SyntaxKind.MethodDeclaration) {
|
|
659
|
+
newTotalDepth++;
|
|
660
|
+
newLogicDepth++;
|
|
661
|
+
if (newLogicDepth > maxLogicNesting)
|
|
662
|
+
maxLogicNesting = newLogicDepth;
|
|
663
|
+
}
|
|
664
|
+
if (newTotalDepth > maxNesting)
|
|
665
|
+
maxNesting = newTotalDepth;
|
|
666
|
+
ts.forEachChild(node, child => visit(child, newTotalDepth, newJsxDepth, newLogicDepth));
|
|
667
|
+
}
|
|
668
|
+
visit(sourceFile, 0, 0, 0);
|
|
669
|
+
return {
|
|
670
|
+
maxNesting,
|
|
671
|
+
jsxNesting: maxJsxNesting,
|
|
672
|
+
logicNesting: maxLogicNesting,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function calculateAdjustedNesting(jsxNesting, logicNesting, config, adjustments) {
|
|
676
|
+
// JSX 중첩에 가중치 적용
|
|
677
|
+
const adjustedJsx = Math.ceil(jsxNesting * config.jsxNestingWeight);
|
|
678
|
+
if (jsxNesting > 0 && config.jsxNestingWeight < 1) {
|
|
679
|
+
adjustments.push({
|
|
680
|
+
type: 'jsx_nesting',
|
|
681
|
+
original: jsxNesting,
|
|
682
|
+
adjusted: adjustedJsx,
|
|
683
|
+
reason: `JSX nesting weighted by ${config.jsxNestingWeight}`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// 최종 중첩 = 로직 중첩 + 보정된 JSX 중첩
|
|
687
|
+
return logicNesting + adjustedJsx;
|
|
688
|
+
}
|
|
689
|
+
function analyzeFunctionsWithFramework(sourceFile, source, frameworkConfig, frameworkInfo) {
|
|
690
|
+
const functions = [];
|
|
691
|
+
function visit(node) {
|
|
692
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
693
|
+
ts.isMethodDeclaration(node) ||
|
|
694
|
+
ts.isArrowFunction(node) ||
|
|
695
|
+
ts.isFunctionExpression(node)) {
|
|
696
|
+
const info = analyzeFunctionWithFramework(node, sourceFile, source, frameworkConfig, frameworkInfo);
|
|
697
|
+
if (info) {
|
|
698
|
+
functions.push(info);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
ts.forEachChild(node, visit);
|
|
702
|
+
}
|
|
703
|
+
visit(sourceFile);
|
|
704
|
+
return functions;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* 파라미터 분석 (개념 수 ~7)
|
|
708
|
+
*/
|
|
709
|
+
function analyzeParameters(node, sourceFile, frameworkConfig, frameworkInfo) {
|
|
710
|
+
const concepts = [];
|
|
711
|
+
const antiPatterns = [];
|
|
712
|
+
if (!node.parameters)
|
|
713
|
+
return { concepts, antiPatterns };
|
|
714
|
+
for (const param of node.parameters) {
|
|
715
|
+
const paramName = param.name.getText(sourceFile);
|
|
716
|
+
if (paramName === 'this')
|
|
717
|
+
continue;
|
|
718
|
+
if (ts.isObjectBindingPattern(param.name)) {
|
|
719
|
+
const bindingCount = param.name.elements.length;
|
|
720
|
+
const adjustedCount = Math.ceil(bindingCount * frameworkConfig.propsDestructureWeight);
|
|
721
|
+
for (let i = 0; i < adjustedCount; i++) {
|
|
722
|
+
concepts.push(`prop:destructured_${i}`);
|
|
723
|
+
}
|
|
724
|
+
if (frameworkConfig.propsDestructureWeight < 1) {
|
|
725
|
+
frameworkInfo.adjustments.push({
|
|
726
|
+
type: 'props',
|
|
727
|
+
original: bindingCount,
|
|
728
|
+
adjusted: adjustedCount,
|
|
729
|
+
reason: `Props destructuring weighted by ${frameworkConfig.propsDestructureWeight}`,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
concepts.push(`param:${paramName}`);
|
|
735
|
+
}
|
|
736
|
+
if (param.dotDotDotToken) {
|
|
737
|
+
antiPatterns.push({
|
|
738
|
+
pattern: 'rest_params',
|
|
739
|
+
penalty: REST_PARAMS_PENALTY,
|
|
740
|
+
reason: `...${paramName} hides actual parameter count`,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return { concepts, antiPatterns };
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 변수 및 Hook 분석 (개념 수 ~8)
|
|
748
|
+
*/
|
|
749
|
+
function analyzeVariablesWithHooks(node, sourceFile, hookPatterns) {
|
|
750
|
+
const variables = new Set();
|
|
751
|
+
let hookCount = 0;
|
|
752
|
+
function visit(n) {
|
|
753
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
|
|
754
|
+
const varText = n.getText(sourceFile);
|
|
755
|
+
const isHook = hookPatterns.some(pattern => pattern.test(varText));
|
|
756
|
+
if (isHook) {
|
|
757
|
+
hookCount++;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
variables.add(n.name.text);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
ts.forEachChild(n, visit);
|
|
764
|
+
}
|
|
765
|
+
if (node.body)
|
|
766
|
+
visit(node.body);
|
|
767
|
+
const concepts = Array.from(variables).map(v => `var:${v}`);
|
|
768
|
+
return { concepts, hookCount };
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* 함수 호출 및 Chain 메서드 분석 (개념 수 ~8)
|
|
772
|
+
*/
|
|
773
|
+
function analyzeCallsWithChains(node, sourceFile) {
|
|
774
|
+
const calls = new Set();
|
|
775
|
+
let chainCount = 0;
|
|
776
|
+
function visit(n) {
|
|
777
|
+
if (ts.isCallExpression(n)) {
|
|
778
|
+
const callName = getCallName(n, sourceFile);
|
|
779
|
+
if (callName && !BUILTIN_FUNCTIONS.has(callName.split('.')[0])) {
|
|
780
|
+
const isChain = CHAIN_METHOD_PATTERNS.some(pattern => pattern.test(callName));
|
|
781
|
+
if (isChain) {
|
|
782
|
+
chainCount++;
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
calls.add(callName);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
ts.forEachChild(n, visit);
|
|
790
|
+
}
|
|
791
|
+
if (node.body)
|
|
792
|
+
visit(node.body);
|
|
793
|
+
const concepts = Array.from(calls).map(c => `call:${c}`);
|
|
794
|
+
return { concepts, chainCount };
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Spread config 안티패턴 분석 (개념 수 ~5)
|
|
798
|
+
*/
|
|
799
|
+
function analyzeSpreadPatterns(node, sourceFile) {
|
|
800
|
+
const antiPatterns = [];
|
|
801
|
+
function visit(n) {
|
|
802
|
+
if (ts.isSpreadElement(n) || ts.isSpreadAssignment(n)) {
|
|
803
|
+
const text = n.getText(sourceFile);
|
|
804
|
+
if (/\.\.\.(options|config|props|params|args)/i.test(text)) {
|
|
805
|
+
antiPatterns.push({
|
|
806
|
+
pattern: 'spread_config',
|
|
807
|
+
penalty: SPREAD_CONFIG_PENALTY,
|
|
808
|
+
reason: `${text} bundles multiple concepts`,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
ts.forEachChild(n, visit);
|
|
813
|
+
}
|
|
814
|
+
if (node.body)
|
|
815
|
+
visit(node.body);
|
|
816
|
+
return antiPatterns;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Hook/Chain 개념에 가중치 적용 (개념 수 ~6)
|
|
820
|
+
*/
|
|
821
|
+
function applyFrameworkWeights(hookCount, chainCount, frameworkConfig, frameworkInfo) {
|
|
822
|
+
const concepts = [];
|
|
823
|
+
const adjustedHookCount = Math.ceil(hookCount * frameworkConfig.hookConceptWeight);
|
|
824
|
+
for (let i = 0; i < adjustedHookCount; i++) {
|
|
825
|
+
concepts.push(`hook:${i}`);
|
|
826
|
+
}
|
|
827
|
+
if (hookCount > 0 && frameworkConfig.hookConceptWeight < 1) {
|
|
828
|
+
frameworkInfo.adjustments.push({
|
|
829
|
+
type: 'hook',
|
|
830
|
+
original: hookCount,
|
|
831
|
+
adjusted: adjustedHookCount,
|
|
832
|
+
reason: `Hook concepts weighted by ${frameworkConfig.hookConceptWeight}`,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const adjustedChainCount = Math.ceil(chainCount * frameworkConfig.chainMethodWeight);
|
|
836
|
+
for (let i = 0; i < adjustedChainCount; i++) {
|
|
837
|
+
concepts.push(`chain:${i}`);
|
|
838
|
+
}
|
|
839
|
+
if (chainCount > 0 && frameworkConfig.chainMethodWeight < 1) {
|
|
840
|
+
frameworkInfo.adjustments.push({
|
|
841
|
+
type: 'chain',
|
|
842
|
+
original: chainCount,
|
|
843
|
+
adjusted: adjustedChainCount,
|
|
844
|
+
reason: `Chain methods weighted by ${frameworkConfig.chainMethodWeight}`,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return concepts;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* 함수 분석 메인 (개념 수 ~8) - 조합만 수행
|
|
851
|
+
*/
|
|
852
|
+
function analyzeFunctionWithFramework(node, sourceFile, _source, frameworkConfig, frameworkInfo) {
|
|
853
|
+
const name = getFunctionName(node);
|
|
854
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
855
|
+
const hookPatterns = HOOK_PATTERNS[frameworkConfig.name];
|
|
856
|
+
// 분리된 분석 함수 호출
|
|
857
|
+
const paramResult = analyzeParameters(node, sourceFile, frameworkConfig, frameworkInfo);
|
|
858
|
+
const varResult = analyzeVariablesWithHooks(node, sourceFile, hookPatterns);
|
|
859
|
+
const callResult = analyzeCallsWithChains(node, sourceFile);
|
|
860
|
+
const spreadPatterns = analyzeSpreadPatterns(node, sourceFile);
|
|
861
|
+
const weightedConcepts = applyFrameworkWeights(varResult.hookCount, callResult.chainCount, frameworkConfig, frameworkInfo);
|
|
862
|
+
// 결과 조합
|
|
863
|
+
const concepts = [
|
|
864
|
+
...paramResult.concepts,
|
|
865
|
+
...varResult.concepts,
|
|
866
|
+
...callResult.concepts,
|
|
867
|
+
...weightedConcepts,
|
|
868
|
+
];
|
|
869
|
+
const antiPatterns = [...paramResult.antiPatterns, ...spreadPatterns];
|
|
870
|
+
const rawConceptCount = concepts.length;
|
|
871
|
+
const penaltyTotal = antiPatterns.reduce((sum, ap) => sum + ap.penalty, 0);
|
|
872
|
+
return {
|
|
873
|
+
name,
|
|
874
|
+
line,
|
|
875
|
+
conceptCount: rawConceptCount + penaltyTotal,
|
|
876
|
+
rawConceptCount,
|
|
877
|
+
concepts,
|
|
878
|
+
antiPatterns,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=cheese.js.map
|