Skip to content

Commit 6c04a47

Browse files
talionwarclaude
andcommitted
fix: address 5 issues found in audit review
- prisma-parser: revert implicit M:N detection (riga 122) — cannot distinguish 1:N from M:N without two-pass model analysis. Only explicit @relation without fields:[] is reliably M:N (riga 102). - prisma-parser: extractBlocks now strips comments before parsing and skips braces inside double-quoted strings (@default("{}")) - project-analyzer: filter $-prefixed prisma operations from models[] ($transaction is not a model name) - components-generator: replace truncation with skip for long values (avoids breaking Markdown with unclosed backticks) - project-analyzer: handle empty file in lineCount (return 0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a3516c9 commit 6c04a47

3 files changed

Lines changed: 36 additions & 13 deletions

File tree

src/analyzers/project-analyzer.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ function buildGraph(
181181
? 'component'
182182
: 'lib';
183183

184-
const lineCount = file.rawContent.endsWith('\n')
185-
? file.rawContent.split('\n').length - 1
186-
: file.rawContent.split('\n').length;
184+
const lineCount = file.rawContent.length === 0
185+
? 0
186+
: file.rawContent.endsWith('\n')
187+
? file.rawContent.split('\n').length - 1
188+
: file.rawContent.split('\n').length;
187189

188190
nodes.set(file.relativePath, {
189191
id: file.relativePath,
@@ -370,7 +372,8 @@ function buildApiRouteInfo(file: ParsedFile, framework: Framework): ApiRouteInfo
370372
const models: string[] = [];
371373
const prismaMatch = file.rawContent.matchAll(/prisma\.(\$?\w+)\./g);
372374
for (const m of prismaMatch) {
373-
models.push(m[1]);
375+
// Skip client-level operations ($transaction, $queryRaw) — not model names
376+
if (!m[1].startsWith('$')) models.push(m[1]);
374377
}
375378

376379
return {

src/generators/components-generator.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,11 @@ function generateComponentEntry(comp: ComponentInfo, ctx: GeneratorContext): str
8989
// State
9090
if (comp.state.length > 0) {
9191
const stateLines = comp.state.map((s) => {
92-
const val = s.initialValue
93-
? ` = ${s.initialValue.replace(/[\n\r]/g, ' ').replace(/\s+/g, ' ').slice(0, 60)}`
94-
: '';
95-
return `\`${s.name}\`${val}`;
92+
if (!s.initialValue) return `\`${s.name}\``;
93+
const cleaned = s.initialValue.replace(/[\n\r]/g, ' ').replace(/\s+/g, ' ');
94+
// Skip long values entirely to avoid truncating mid-backtick/brace
95+
const val = cleaned.length > 50 ? '(...)' : cleaned;
96+
return `\`${s.name}\` = ${val}`;
9697
});
9798
output += `${bulletList('Internal State', stateLines)}\n`;
9899
}

src/schema/prisma-parser.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,37 @@ const PRISMA_SCALAR_TYPES = new Set([
1212
'String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'BigInt', 'Decimal', 'Bytes',
1313
]);
1414

15+
/**
16+
* Strip comments from Prisma schema content to prevent false matches
17+
* in extractBlocks (keywords/braces inside comments)
18+
*/
19+
function stripComments(content: string): string {
20+
return content
21+
.replace(/\/\*[\s\S]*?\*\//g, '') // multi-line /* */
22+
.replace(/\/\/.*$/gm, ''); // single-line //
23+
}
24+
1525
/**
1626
* Extract brace-balanced blocks for a given keyword (model, enum, etc.)
1727
* Handles nested braces correctly — unlike [^}]+ regex which fails on @default({})
28+
* Skips braces inside double-quoted strings to handle @default("{}") correctly
1829
*/
19-
function extractBlocks(content: string, keyword: string): { name: string; body: string }[] {
30+
function extractBlocks(rawContent: string, keyword: string): { name: string; body: string }[] {
31+
const content = stripComments(rawContent);
2032
const blocks: { name: string; body: string }[] = [];
2133
const headerRegex = new RegExp(`${keyword}\\s+(\\w+)\\s*\\{`, 'g');
2234
let match;
2335
while ((match = headerRegex.exec(content)) !== null) {
2436
let depth = 1;
2537
let i = match.index + match[0].length;
38+
let inString = false;
2639
while (i < content.length && depth > 0) {
27-
if (content[i] === '{') depth++;
28-
else if (content[i] === '}') depth--;
40+
if (content[i] === '"' && content[i - 1] !== '\\') {
41+
inString = !inString;
42+
} else if (!inString) {
43+
if (content[i] === '{') depth++;
44+
else if (content[i] === '}') depth--;
45+
}
2946
i++;
3047
}
3148
if (depth === 0) {
@@ -124,11 +141,13 @@ function parseModelBlock(name: string, body: string, enumNames: Set<string>): Sc
124141
) {
125142
if (!relations.some((r) => r.field === fieldName)) {
126143
const isArray = line.includes('[]');
127-
const isManyToMany = isArray && !line.includes('@relation');
144+
// Implicit relations without @relation: default to one-to-many for arrays.
145+
// Cannot distinguish 1:N from M:N without checking if target model has FK back.
146+
// M:N detection only works reliably on explicit @relation (see riga 102 above).
128147
relations.push({
129148
field: fieldName,
130149
target: fieldType,
131-
type: isManyToMany ? 'many-to-many' : isArray ? 'one-to-many' : 'one-to-one',
150+
type: isArray ? 'one-to-many' : 'one-to-one',
132151
});
133152
}
134153
}

0 commit comments

Comments
 (0)