@kernlang/review 2.0.0
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/LICENSE +661 -0
- package/dist/differ.d.ts +11 -0
- package/dist/differ.js +132 -0
- package/dist/differ.js.map +1 -0
- package/dist/external-tools.d.ts +32 -0
- package/dist/external-tools.js +173 -0
- package/dist/external-tools.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/dist/inferrer.d.ts +15 -0
- package/dist/inferrer.js +502 -0
- package/dist/inferrer.js.map +1 -0
- package/dist/llm-review.d.ts +24 -0
- package/dist/llm-review.js +197 -0
- package/dist/llm-review.js.map +1 -0
- package/dist/quality-rules.d.ts +12 -0
- package/dist/quality-rules.js +28 -0
- package/dist/quality-rules.js.map +1 -0
- package/dist/reporter.d.ts +15 -0
- package/dist/reporter.js +217 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules/base.d.ts +10 -0
- package/dist/rules/base.js +556 -0
- package/dist/rules/base.js.map +1 -0
- package/dist/rules/express.d.ts +9 -0
- package/dist/rules/express.js +107 -0
- package/dist/rules/express.js.map +1 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +38 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/nextjs.d.ts +9 -0
- package/dist/rules/nextjs.js +128 -0
- package/dist/rules/nextjs.js.map +1 -0
- package/dist/rules/react.d.ts +9 -0
- package/dist/rules/react.js +252 -0
- package/dist/rules/react.js.map +1 -0
- package/dist/rules/vue.d.ts +9 -0
- package/dist/rules/vue.js +198 -0
- package/dist/rules/vue.js.map +1 -0
- package/dist/template-detector.d.ts +12 -0
- package/dist/template-detector.js +225 -0
- package/dist/template-detector.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/package.json +23 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue review rules — active when target = vue | nuxt.
|
|
3
|
+
*
|
|
4
|
+
* Catches Vue 3 Composition API pitfalls.
|
|
5
|
+
*/
|
|
6
|
+
import { SyntaxKind } from 'ts-morph';
|
|
7
|
+
import { createFingerprint } from '../types.js';
|
|
8
|
+
function span(file, line, col = 1) {
|
|
9
|
+
return { file, startLine: line, startCol: col, endLine: line, endCol: col };
|
|
10
|
+
}
|
|
11
|
+
function finding(ruleId, severity, category, message, file, line, extra) {
|
|
12
|
+
return {
|
|
13
|
+
source: 'kern',
|
|
14
|
+
ruleId,
|
|
15
|
+
severity,
|
|
16
|
+
category,
|
|
17
|
+
message,
|
|
18
|
+
primarySpan: span(file, line),
|
|
19
|
+
fingerprint: createFingerprint(ruleId, line, 1),
|
|
20
|
+
...extra,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// ── Rule 17: missing-ref-value ───────────────────────────────────────────
|
|
24
|
+
// Using ref() result without .value in script setup
|
|
25
|
+
function missingRefValue(ctx) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
// AST-based: find ref() declarations via variable statements
|
|
28
|
+
const refVarNames = new Map(); // name → declaration line
|
|
29
|
+
for (const stmt of ctx.sourceFile.getVariableStatements()) {
|
|
30
|
+
for (const decl of stmt.getDeclarations()) {
|
|
31
|
+
const init = decl.getInitializer();
|
|
32
|
+
if (!init)
|
|
33
|
+
continue;
|
|
34
|
+
// Match ref() or ref<T>() calls
|
|
35
|
+
if (init.getKind() === SyntaxKind.CallExpression) {
|
|
36
|
+
const call = init;
|
|
37
|
+
const calleeName = call.getExpression().getText();
|
|
38
|
+
if (calleeName === 'ref') {
|
|
39
|
+
refVarNames.set(decl.getName(), stmt.getStartLineNumber());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (refVarNames.size === 0)
|
|
45
|
+
return findings;
|
|
46
|
+
// Walk all identifiers and check if ref vars are used without .value
|
|
47
|
+
for (const ident of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
48
|
+
const name = ident.getText();
|
|
49
|
+
if (!refVarNames.has(name))
|
|
50
|
+
continue;
|
|
51
|
+
// Skip the declaration itself
|
|
52
|
+
if (ident.getStartLineNumber() === refVarNames.get(name))
|
|
53
|
+
continue;
|
|
54
|
+
const parent = ident.getParent();
|
|
55
|
+
if (!parent)
|
|
56
|
+
continue;
|
|
57
|
+
// If parent is PropertyAccessExpression and ident is the object, check if accessing .value
|
|
58
|
+
if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
59
|
+
const propAccess = parent;
|
|
60
|
+
if (propAccess.getExpression() === ident) {
|
|
61
|
+
if (propAccess.getName() === 'value')
|
|
62
|
+
continue; // correct: ref.value
|
|
63
|
+
// Accessing some other property on ref without .value — still a bug
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Skip if ref is passed as a function argument (intentional: watch(myRef), toRef(myRef))
|
|
67
|
+
if (parent.getKind() === SyntaxKind.CallExpression)
|
|
68
|
+
continue;
|
|
69
|
+
// Also skip if it's an argument in a call's argument list
|
|
70
|
+
const grandparent = parent.getParent();
|
|
71
|
+
if (grandparent?.getKind() === SyntaxKind.CallExpression)
|
|
72
|
+
continue;
|
|
73
|
+
// Skip type contexts
|
|
74
|
+
if (parent.getKind() === SyntaxKind.TypeReference)
|
|
75
|
+
continue;
|
|
76
|
+
if (parent.getKind() === SyntaxKind.TypeQuery)
|
|
77
|
+
continue;
|
|
78
|
+
// Skip shorthand property assignments: { count } in object literals
|
|
79
|
+
if (parent.getKind() === SyntaxKind.ShorthandPropertyAssignment)
|
|
80
|
+
continue;
|
|
81
|
+
// Skip imports and variable declarations
|
|
82
|
+
if (parent.getKind() === SyntaxKind.ImportSpecifier)
|
|
83
|
+
continue;
|
|
84
|
+
if (parent.getKind() === SyntaxKind.VariableDeclaration)
|
|
85
|
+
continue;
|
|
86
|
+
// Likely a bug: ref used in expression context without .value
|
|
87
|
+
if (parent.getKind() === SyntaxKind.BinaryExpression ||
|
|
88
|
+
parent.getKind() === SyntaxKind.ConditionalExpression ||
|
|
89
|
+
parent.getKind() === SyntaxKind.TemplateSpan ||
|
|
90
|
+
parent.getKind() === SyntaxKind.ReturnStatement ||
|
|
91
|
+
parent.getKind() === SyntaxKind.ElementAccessExpression) {
|
|
92
|
+
findings.push(finding('missing-ref-value', 'warning', 'bug', `'${name}' is a ref — did you mean '${name}.value'?`, ctx.filePath, ident.getStartLineNumber(), { suggestion: `${name}.value` }));
|
|
93
|
+
// One finding per ref variable to avoid noise
|
|
94
|
+
refVarNames.delete(name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
// ── Rule 18: missing-onUnmounted ─────────────────────────────────────────
|
|
100
|
+
// watch/addEventListener without cleanup in onUnmounted
|
|
101
|
+
function missingOnUnmounted(ctx) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
// AST pre-check: find onUnmounted/onBeforeUnmount CallExpressions
|
|
104
|
+
let hasLifecycleCleanup = false;
|
|
105
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
106
|
+
const callee = call.getExpression();
|
|
107
|
+
if (callee.getKind() === SyntaxKind.Identifier) {
|
|
108
|
+
const name = callee.getText();
|
|
109
|
+
if (name === 'onUnmounted' || name === 'onBeforeUnmount') {
|
|
110
|
+
hasLifecycleCleanup = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Check for watch() calls via AST
|
|
116
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
117
|
+
const callee = call.getExpression();
|
|
118
|
+
if (callee.getKind() !== SyntaxKind.Identifier || callee.getText() !== 'watch')
|
|
119
|
+
continue;
|
|
120
|
+
// Check if parent is VariableDeclaration (stop handle assigned)
|
|
121
|
+
const parent = call.getParent();
|
|
122
|
+
const hasStopHandle = parent?.getKind() === SyntaxKind.VariableDeclaration;
|
|
123
|
+
if (!hasStopHandle && !hasLifecycleCleanup) {
|
|
124
|
+
findings.push(finding('missing-onUnmounted', 'error', 'bug', 'watch() without stop handle or onUnmounted cleanup — potential memory leak', ctx.filePath, call.getStartLineNumber(), { suggestion: 'Assign watch to a variable and call stop() in onUnmounted, or use watchEffect (auto-stops)' }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Check for addEventListener via AST
|
|
128
|
+
let hasRemoveListener = false;
|
|
129
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
130
|
+
const callee = call.getExpression();
|
|
131
|
+
if (callee.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
132
|
+
const pa = callee;
|
|
133
|
+
if (pa.getName() === 'removeEventListener') {
|
|
134
|
+
hasRemoveListener = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
140
|
+
const callee = call.getExpression();
|
|
141
|
+
if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
|
|
142
|
+
continue;
|
|
143
|
+
const pa = callee;
|
|
144
|
+
if (pa.getName() !== 'addEventListener')
|
|
145
|
+
continue;
|
|
146
|
+
if (!hasRemoveListener && !hasLifecycleCleanup) {
|
|
147
|
+
findings.push(finding('missing-onUnmounted', 'error', 'bug', 'addEventListener without removeEventListener in onUnmounted — memory leak', ctx.filePath, call.getStartLineNumber(), { suggestion: 'Clean up event listeners in onUnmounted()' }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
// ── Rule 19: setup-side-effect ───────────────────────────────────────────
|
|
153
|
+
// Async call in setup() without onMounted wrapper
|
|
154
|
+
function setupSideEffect(ctx) {
|
|
155
|
+
const findings = [];
|
|
156
|
+
const fullText = ctx.sourceFile.getFullText();
|
|
157
|
+
// Detect <script setup> or setup() function
|
|
158
|
+
const isScriptSetup = fullText.includes('<script setup') || fullText.includes('defineComponent');
|
|
159
|
+
if (!isScriptSetup)
|
|
160
|
+
return findings;
|
|
161
|
+
// Check for top-level await without onMounted
|
|
162
|
+
const hasOnMounted = fullText.includes('onMounted');
|
|
163
|
+
const awaitRegex = /(?:^|\n)\s*(?:const|let|var)?\s*\w*\s*=?\s*await\s/g;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = awaitRegex.exec(fullText)) !== null) {
|
|
166
|
+
const line = fullText.substring(0, match.index).split('\n').length;
|
|
167
|
+
// Skip if inside a function body
|
|
168
|
+
const lineText = fullText.split('\n')[line - 1] || '';
|
|
169
|
+
if (lineText.trim().startsWith('//') || lineText.trim().startsWith('*'))
|
|
170
|
+
continue;
|
|
171
|
+
if (!hasOnMounted) {
|
|
172
|
+
findings.push(finding('setup-side-effect', 'warning', 'pattern', 'Top-level await in setup — consider wrapping in onMounted() for SSR compatibility', ctx.filePath, line, { suggestion: 'onMounted(async () => { ... })' }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return findings;
|
|
176
|
+
}
|
|
177
|
+
// ── Rule 20: reactive-destructure ────────────────────────────────────────
|
|
178
|
+
// Destructuring reactive() loses reactivity
|
|
179
|
+
function reactiveDestructure(ctx) {
|
|
180
|
+
const findings = [];
|
|
181
|
+
const fullText = ctx.sourceFile.getFullText();
|
|
182
|
+
// Find const { ... } = reactive(...)
|
|
183
|
+
const destructRegex = /(?:const|let)\s*\{[^}]+\}\s*=\s*reactive\s*\(/g;
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = destructRegex.exec(fullText)) !== null) {
|
|
186
|
+
const line = fullText.substring(0, match.index).split('\n').length;
|
|
187
|
+
findings.push(finding('reactive-destructure', 'warning', 'bug', 'Destructuring reactive() loses reactivity — use toRefs() or access properties directly', ctx.filePath, line, { suggestion: 'const state = reactive({...}); use state.prop, or const { prop } = toRefs(state)' }));
|
|
188
|
+
}
|
|
189
|
+
return findings;
|
|
190
|
+
}
|
|
191
|
+
// ── Exported Vue Rules ───────────────────────────────────────────────────
|
|
192
|
+
export const vueRules = [
|
|
193
|
+
missingRefValue,
|
|
194
|
+
missingOnUnmounted,
|
|
195
|
+
setupSideEffect,
|
|
196
|
+
reactiveDestructure,
|
|
197
|
+
];
|
|
198
|
+
//# sourceMappingURL=vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue.js","sourceRoot":"","sources":["../../src/rules/vue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Detector — matches code against known library patterns.
|
|
3
|
+
*
|
|
4
|
+
* Import-anchored detection: look at import sources to identify libraries,
|
|
5
|
+
* then pattern-match the usage and extract slot values for .kern rewrites.
|
|
6
|
+
*
|
|
7
|
+
* When registeredTemplates are provided (from kern.config.ts), the detector
|
|
8
|
+
* generates suggested .kern rewrites with actual slot values filled in.
|
|
9
|
+
*/
|
|
10
|
+
import { SourceFile } from 'ts-morph';
|
|
11
|
+
import type { TemplateMatch, ReviewConfig } from './types.js';
|
|
12
|
+
export declare function detectTemplates(sourceFile: SourceFile, config?: ReviewConfig): TemplateMatch[];
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Detector — matches code against known library patterns.
|
|
3
|
+
*
|
|
4
|
+
* Import-anchored detection: look at import sources to identify libraries,
|
|
5
|
+
* then pattern-match the usage and extract slot values for .kern rewrites.
|
|
6
|
+
*
|
|
7
|
+
* When registeredTemplates are provided (from kern.config.ts), the detector
|
|
8
|
+
* generates suggested .kern rewrites with actual slot values filled in.
|
|
9
|
+
*/
|
|
10
|
+
import { countTokens } from '@kernlang/core';
|
|
11
|
+
// ── Slot Extractors ──────────────────────────────────────────────────────
|
|
12
|
+
function extractZustandSlots(sourceFile, fullText) {
|
|
13
|
+
// Pattern: const useXStore = create<StateType>((set, get) => ({...}))
|
|
14
|
+
// or: export const useXStore = create<StateType>()(...)
|
|
15
|
+
const match = fullText.match(/(?:export\s+)?const\s+use(\w+)Store\s*=\s*create\s*<\s*(\w+)\s*>/);
|
|
16
|
+
if (match) {
|
|
17
|
+
return { storeName: match[1], stateType: match[2] };
|
|
18
|
+
}
|
|
19
|
+
// Fallback: look for create<X> without the useXStore naming
|
|
20
|
+
const match2 = fullText.match(/create\s*<\s*(\w+)\s*>/);
|
|
21
|
+
if (match2) {
|
|
22
|
+
// Try to find the variable name
|
|
23
|
+
const varMatch = fullText.match(/(?:export\s+)?const\s+(\w+)\s*=\s*create/);
|
|
24
|
+
const storeName = varMatch ? varMatch[1].replace(/^use/, '').replace(/Store$/, '') : match2[1];
|
|
25
|
+
return { storeName, stateType: match2[1] };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function extractSwrSlots(sourceFile, fullText) {
|
|
30
|
+
// Pattern: function useX() { ... useSWR(key, fetcher) ... }
|
|
31
|
+
const fnMatch = fullText.match(/function\s+(use\w+)\s*\(/);
|
|
32
|
+
const swrMatch = fullText.match(/useSWR\s*\(\s*([^,)]+)/);
|
|
33
|
+
if (fnMatch && swrMatch) {
|
|
34
|
+
return { hookName: fnMatch[1], cacheKey: swrMatch[1].trim() };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function extractQuerySlots(sourceFile, fullText) {
|
|
39
|
+
const fnMatch = fullText.match(/function\s+(use\w+)\s*\(/);
|
|
40
|
+
const keyMatch = fullText.match(/queryKey\s*:\s*\[([^\]]+)\]/);
|
|
41
|
+
const fnBodyMatch = fullText.match(/queryFn\s*:\s*(\w+)/);
|
|
42
|
+
if (fnMatch && keyMatch) {
|
|
43
|
+
return {
|
|
44
|
+
hookName: fnMatch[1],
|
|
45
|
+
queryKey: keyMatch[1].trim(),
|
|
46
|
+
...(fnBodyMatch ? { queryFn: fnBodyMatch[1] } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function extractJotaiSlots(sourceFile, fullText) {
|
|
52
|
+
const match = fullText.match(/(?:export\s+)?const\s+(\w+)Atom\s*=\s*atom\s*<\s*(\w+)\s*>\s*\(\s*([^)]+)\s*\)/);
|
|
53
|
+
if (match) {
|
|
54
|
+
return { atomName: match[1], atomType: match[2], initialValue: match[3].trim() };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function extractZodSlots(sourceFile, fullText) {
|
|
59
|
+
const match = fullText.match(/(?:export\s+)?const\s+(\w+)(?:Schema)?\s*=\s*z\.object\s*\(/);
|
|
60
|
+
if (match) {
|
|
61
|
+
return { schemaName: match[1] };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// ── Patterns ─────────────────────────────────────────────────────────────
|
|
66
|
+
const PATTERNS = [
|
|
67
|
+
{
|
|
68
|
+
templateName: 'zustand-store',
|
|
69
|
+
libraryName: 'zustand',
|
|
70
|
+
importSource: 'zustand',
|
|
71
|
+
anchorImport: 'create',
|
|
72
|
+
bodyHint: /create\s*(<|[\s(])/,
|
|
73
|
+
confidencePct: 92,
|
|
74
|
+
extractSlots: extractZustandSlots,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
templateName: 'zustand-selector',
|
|
78
|
+
libraryName: 'zustand',
|
|
79
|
+
importSource: 'zustand',
|
|
80
|
+
anchorImport: 'create',
|
|
81
|
+
bodyHint: /useStore\s*\(\s*\(\s*state\s*\)/,
|
|
82
|
+
confidencePct: 85,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
templateName: 'swr-hook',
|
|
86
|
+
libraryName: 'SWR',
|
|
87
|
+
importSource: 'swr',
|
|
88
|
+
anchorImport: 'useSWR',
|
|
89
|
+
bodyHint: /useSWR\s*\(/,
|
|
90
|
+
confidencePct: 90,
|
|
91
|
+
extractSlots: extractSwrSlots,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
templateName: 'query-hook',
|
|
95
|
+
libraryName: 'TanStack Query',
|
|
96
|
+
importSource: /tanstack\/.*query/,
|
|
97
|
+
anchorImport: 'useQuery',
|
|
98
|
+
bodyHint: /useQuery\s*\(\s*\{/,
|
|
99
|
+
confidencePct: 90,
|
|
100
|
+
extractSlots: extractQuerySlots,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
templateName: 'mutation-hook',
|
|
104
|
+
libraryName: 'TanStack Query',
|
|
105
|
+
importSource: /tanstack\/.*query/,
|
|
106
|
+
anchorImport: 'useMutation',
|
|
107
|
+
bodyHint: /useMutation\s*\(\s*\{/,
|
|
108
|
+
confidencePct: 88,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
templateName: 'xstate-machine',
|
|
112
|
+
libraryName: 'XState',
|
|
113
|
+
importSource: 'xstate',
|
|
114
|
+
anchorImport: 'createMachine',
|
|
115
|
+
bodyHint: /createMachine\s*\(/,
|
|
116
|
+
confidencePct: 90,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
templateName: 'jotai-atom',
|
|
120
|
+
libraryName: 'Jotai',
|
|
121
|
+
importSource: 'jotai',
|
|
122
|
+
anchorImport: 'atom',
|
|
123
|
+
bodyHint: /atom\s*(<|\()/,
|
|
124
|
+
confidencePct: 88,
|
|
125
|
+
extractSlots: extractJotaiSlots,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
templateName: 'trpc-router',
|
|
129
|
+
libraryName: 'tRPC',
|
|
130
|
+
importSource: /@trpc/,
|
|
131
|
+
anchorImport: 'router',
|
|
132
|
+
bodyHint: /\.router\s*\(/,
|
|
133
|
+
confidencePct: 85,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
templateName: 'zod-schema',
|
|
137
|
+
libraryName: 'Zod',
|
|
138
|
+
importSource: 'zod',
|
|
139
|
+
anchorImport: 'z',
|
|
140
|
+
bodyHint: /z\.object\s*\(/,
|
|
141
|
+
confidencePct: 88,
|
|
142
|
+
extractSlots: extractZodSlots,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
// ── Build .kern suggestion ───────────────────────────────────────────────
|
|
146
|
+
function buildKernSuggestion(templateName, slots) {
|
|
147
|
+
const parts = [templateName];
|
|
148
|
+
for (const [key, value] of Object.entries(slots)) {
|
|
149
|
+
if (value.includes(' ')) {
|
|
150
|
+
parts.push(`${key}="${value}"`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
parts.push(`${key}=${value}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return parts.join(' ');
|
|
157
|
+
}
|
|
158
|
+
// ── Main Detector ────────────────────────────────────────────────────────
|
|
159
|
+
export function detectTemplates(sourceFile, config) {
|
|
160
|
+
const results = [];
|
|
161
|
+
const fullText = sourceFile.getFullText();
|
|
162
|
+
const totalTokens = countTokens(fullText);
|
|
163
|
+
const imports = sourceFile.getImportDeclarations();
|
|
164
|
+
const registeredTemplates = new Set(config?.registeredTemplates || []);
|
|
165
|
+
for (const pattern of PATTERNS) {
|
|
166
|
+
const matchingImport = imports.find(imp => {
|
|
167
|
+
const source = imp.getModuleSpecifierValue();
|
|
168
|
+
if (typeof pattern.importSource === 'string') {
|
|
169
|
+
return source === pattern.importSource || source.startsWith(pattern.importSource + '/');
|
|
170
|
+
}
|
|
171
|
+
return pattern.importSource.test(source);
|
|
172
|
+
});
|
|
173
|
+
if (!matchingImport)
|
|
174
|
+
continue;
|
|
175
|
+
const namedImports = matchingImport.getNamedImports().map(n => n.getName());
|
|
176
|
+
const defaultImport = matchingImport.getDefaultImport()?.getText();
|
|
177
|
+
const hasAnchor = namedImports.includes(pattern.anchorImport) ||
|
|
178
|
+
defaultImport === pattern.anchorImport;
|
|
179
|
+
if (!hasAnchor)
|
|
180
|
+
continue;
|
|
181
|
+
let confidence = pattern.confidencePct;
|
|
182
|
+
if (pattern.bodyHint) {
|
|
183
|
+
const bodyMatches = typeof pattern.bodyHint === 'string'
|
|
184
|
+
? fullText.includes(pattern.bodyHint)
|
|
185
|
+
: pattern.bodyHint.test(fullText);
|
|
186
|
+
if (!bodyMatches) {
|
|
187
|
+
confidence -= 15;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (confidence < 50)
|
|
191
|
+
continue;
|
|
192
|
+
const startLine = matchingImport.getStartLineNumber();
|
|
193
|
+
const endLine = sourceFile.getEndLineNumber();
|
|
194
|
+
// Extract slot values if extractor exists
|
|
195
|
+
const slotValues = pattern.extractSlots
|
|
196
|
+
? pattern.extractSlots(sourceFile, fullText) ?? undefined
|
|
197
|
+
: undefined;
|
|
198
|
+
// Build .kern suggestion if template is registered and slots extracted
|
|
199
|
+
let suggestedKern;
|
|
200
|
+
let kernTokens;
|
|
201
|
+
const isRegistered = registeredTemplates.has(pattern.templateName);
|
|
202
|
+
if (slotValues) {
|
|
203
|
+
suggestedKern = buildKernSuggestion(pattern.templateName, slotValues);
|
|
204
|
+
kernTokens = countTokens(suggestedKern);
|
|
205
|
+
// Boost confidence when template is registered
|
|
206
|
+
if (isRegistered) {
|
|
207
|
+
confidence = Math.min(confidence + 5, 99);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
results.push({
|
|
211
|
+
templateName: pattern.templateName,
|
|
212
|
+
libraryName: pattern.libraryName,
|
|
213
|
+
anchorImport: pattern.anchorImport,
|
|
214
|
+
confidencePct: confidence,
|
|
215
|
+
startLine,
|
|
216
|
+
endLine,
|
|
217
|
+
slotValues,
|
|
218
|
+
suggestedKern,
|
|
219
|
+
kernTokens,
|
|
220
|
+
tsTokens: totalTokens,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return results;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=template-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-detector.js","sourceRoot":"","sources":["../src/template-detector.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @kernlang/review — TS → .kern inference, review pipeline, unified findings.
|
|
3
|
+
*
|
|
4
|
+
* v2: Unified ReviewFinding replaces QualityFinding + DiffFinding.
|
|
5
|
+
* InferResult gains stable nodeId + promptAlias + sourceSpans.
|
|
6
|
+
*/
|
|
7
|
+
import type { IRNode } from '@kernlang/core';
|
|
8
|
+
/** Exact location in a source file */
|
|
9
|
+
export interface SourceSpan {
|
|
10
|
+
file: string;
|
|
11
|
+
startLine: number;
|
|
12
|
+
startCol: number;
|
|
13
|
+
endLine: number;
|
|
14
|
+
endCol: number;
|
|
15
|
+
}
|
|
16
|
+
/** Unified finding from any review layer */
|
|
17
|
+
export interface ReviewFinding {
|
|
18
|
+
/** Which layer produced this finding */
|
|
19
|
+
source: 'kern' | 'eslint' | 'tsc' | 'llm';
|
|
20
|
+
/** Rule identifier (e.g., 'memory-leak', 'floating-promise') */
|
|
21
|
+
ruleId: string;
|
|
22
|
+
/** Severity level */
|
|
23
|
+
severity: 'error' | 'warning' | 'info';
|
|
24
|
+
/** Finding category */
|
|
25
|
+
category: 'bug' | 'type' | 'pattern' | 'style' | 'structure';
|
|
26
|
+
/** Human-readable message */
|
|
27
|
+
message: string;
|
|
28
|
+
/** Primary source location */
|
|
29
|
+
primarySpan: SourceSpan;
|
|
30
|
+
/** Related locations (e.g., definition + usage) */
|
|
31
|
+
relatedSpans?: SourceSpan[];
|
|
32
|
+
/** Associated KERN nodeIds */
|
|
33
|
+
nodeIds?: string[];
|
|
34
|
+
/** Fix suggestion */
|
|
35
|
+
suggestion?: string;
|
|
36
|
+
/** Confidence (0-1, for LLM findings) */
|
|
37
|
+
confidence?: number;
|
|
38
|
+
/** Stable fingerprint for dedup across sources */
|
|
39
|
+
fingerprint: string;
|
|
40
|
+
}
|
|
41
|
+
/** Confidence level for an inference match */
|
|
42
|
+
export type Confidence = 'high' | 'medium' | 'low';
|
|
43
|
+
/** Result of inferring a single TS construct as a KERN node */
|
|
44
|
+
export interface InferResult {
|
|
45
|
+
/** The inferred KERN IR node */
|
|
46
|
+
node: IRNode;
|
|
47
|
+
/** Stable internal ID: file#type:name@offset */
|
|
48
|
+
nodeId: string;
|
|
49
|
+
/** Short alias for LLM prompts: "N1", "N2", etc. (assigned after sort) */
|
|
50
|
+
promptAlias: string;
|
|
51
|
+
/** Source location in the original TS file */
|
|
52
|
+
startLine: number;
|
|
53
|
+
endLine: number;
|
|
54
|
+
/** Exact TS source spans for this construct */
|
|
55
|
+
sourceSpans: SourceSpan[];
|
|
56
|
+
/** What was detected (human-readable summary) */
|
|
57
|
+
summary: string;
|
|
58
|
+
/** Confidence of the inference */
|
|
59
|
+
confidence: Confidence;
|
|
60
|
+
/** Confidence percentage (0-100) */
|
|
61
|
+
confidencePct: number;
|
|
62
|
+
/** KERN token count for this construct */
|
|
63
|
+
kernTokens: number;
|
|
64
|
+
/** Original TS token count for this construct */
|
|
65
|
+
tsTokens: number;
|
|
66
|
+
}
|
|
67
|
+
/** A template pattern match */
|
|
68
|
+
export interface TemplateMatch {
|
|
69
|
+
/** Template name (e.g., 'zustand-store', 'swr-hook') */
|
|
70
|
+
templateName: string;
|
|
71
|
+
/** Library name (e.g., 'zustand', 'swr') */
|
|
72
|
+
libraryName: string;
|
|
73
|
+
/** Import that anchored the detection */
|
|
74
|
+
anchorImport: string;
|
|
75
|
+
/** Confidence percentage */
|
|
76
|
+
confidencePct: number;
|
|
77
|
+
/** Source location */
|
|
78
|
+
startLine: number;
|
|
79
|
+
endLine: number;
|
|
80
|
+
/** Suggested .kern rewrite (when template is registered) */
|
|
81
|
+
suggestedKern?: string;
|
|
82
|
+
/** Extracted slot values from code analysis */
|
|
83
|
+
slotValues?: Record<string, string>;
|
|
84
|
+
/** Estimated KERN tokens for the suggested rewrite */
|
|
85
|
+
kernTokens?: number;
|
|
86
|
+
/** Original TS tokens covered by this match */
|
|
87
|
+
tsTokens?: number;
|
|
88
|
+
}
|
|
89
|
+
/** Full review report for a single file */
|
|
90
|
+
export interface ReviewReport {
|
|
91
|
+
/** File path that was reviewed */
|
|
92
|
+
filePath: string;
|
|
93
|
+
/** Inferred KERN constructs */
|
|
94
|
+
inferred: InferResult[];
|
|
95
|
+
/** Template pattern matches */
|
|
96
|
+
templateMatches: TemplateMatch[];
|
|
97
|
+
/** All findings from every review layer (unified) */
|
|
98
|
+
findings: ReviewFinding[];
|
|
99
|
+
/** Summary stats */
|
|
100
|
+
stats: ReviewStats;
|
|
101
|
+
}
|
|
102
|
+
/** Summary statistics for a review */
|
|
103
|
+
export interface ReviewStats {
|
|
104
|
+
/** Total lines in the original file */
|
|
105
|
+
totalLines: number;
|
|
106
|
+
/** Lines covered by KERN inferences */
|
|
107
|
+
coveredLines: number;
|
|
108
|
+
/** Coverage percentage */
|
|
109
|
+
coveragePct: number;
|
|
110
|
+
/** Total TS tokens */
|
|
111
|
+
totalTsTokens: number;
|
|
112
|
+
/** Total KERN tokens (if re-expressed as .kern) */
|
|
113
|
+
totalKernTokens: number;
|
|
114
|
+
/** Token reduction percentage */
|
|
115
|
+
reductionPct: number;
|
|
116
|
+
/** Number of KERN-expressible constructs */
|
|
117
|
+
constructCount: number;
|
|
118
|
+
}
|
|
119
|
+
/** Enforcement result for CI */
|
|
120
|
+
export interface EnforceResult {
|
|
121
|
+
/** Whether enforcement passed */
|
|
122
|
+
passed: boolean;
|
|
123
|
+
/** Minimum coverage threshold */
|
|
124
|
+
minCoverage: number;
|
|
125
|
+
/** Actual coverage */
|
|
126
|
+
actualCoverage: number;
|
|
127
|
+
/** Template violations (detected pattern but no KERN template used) */
|
|
128
|
+
templateViolations: string[];
|
|
129
|
+
}
|
|
130
|
+
/** Configuration for the review pipeline */
|
|
131
|
+
export interface ReviewConfig {
|
|
132
|
+
/** Registered template names (from kern.config.ts templates) */
|
|
133
|
+
registeredTemplates?: string[];
|
|
134
|
+
/** Minimum coverage for enforcement */
|
|
135
|
+
minCoverage?: number;
|
|
136
|
+
/** Require detected library patterns to use KERN templates */
|
|
137
|
+
enforceTemplates?: boolean;
|
|
138
|
+
/** Build target — activates framework-specific rules */
|
|
139
|
+
target?: string;
|
|
140
|
+
}
|
|
141
|
+
/** Context passed to each review rule */
|
|
142
|
+
export interface RuleContext {
|
|
143
|
+
sourceFile: import('ts-morph').SourceFile;
|
|
144
|
+
inferred: InferResult[];
|
|
145
|
+
templateMatches: TemplateMatch[];
|
|
146
|
+
config?: ReviewConfig;
|
|
147
|
+
filePath: string;
|
|
148
|
+
}
|
|
149
|
+
/** A review rule function */
|
|
150
|
+
export type ReviewRule = (ctx: RuleContext) => ReviewFinding[];
|
|
151
|
+
/** Create a stable fingerprint for dedup across sources and runs */
|
|
152
|
+
export declare function createFingerprint(ruleId: string, startLine: number, startCol: number): string;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @kernlang/review — TS → .kern inference, review pipeline, unified findings.
|
|
3
|
+
*
|
|
4
|
+
* v2: Unified ReviewFinding replaces QualityFinding + DiffFinding.
|
|
5
|
+
* InferResult gains stable nodeId + promptAlias + sourceSpans.
|
|
6
|
+
*/
|
|
7
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
8
|
+
/** Create a stable fingerprint for dedup across sources and runs */
|
|
9
|
+
export function createFingerprint(ruleId, startLine, startCol) {
|
|
10
|
+
const input = `${ruleId}:${startLine}:${startCol}`;
|
|
11
|
+
let hash = 0;
|
|
12
|
+
for (let i = 0; i < input.length; i++) {
|
|
13
|
+
hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
|
|
14
|
+
}
|
|
15
|
+
return Math.abs(hash).toString(36);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA8KH,4EAA4E;AAE5E,oEAAoE;AACpE,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,SAAiB,EAAE,QAAgB;IACnF,MAAM,KAAK,GAAG,GAAG,MAAM,IAAI,SAAS,IAAI,QAAQ,EAAE,CAAC;IACnD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACrC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernlang/review",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Kern Review — scan TS, infer .kern IR, roundtrip diff, report",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"license": "AGPL-3.0",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ts-morph": "^24.0.0",
|
|
17
|
+
"@kernlang/core": "2.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -b",
|
|
21
|
+
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --forceExit --config jest.config.js"
|
|
22
|
+
}
|
|
23
|
+
}
|