Skip to content

Commit 23f21f2

Browse files
Brooooooklynclaude
andauthored
fix(compare): normalize numeric literals and object literal formatting (#54)
Add two normalizations to the comparison tool to reduce false positives: - Numeric literals: AST-based normalization using oxc-parser converts scientific notation (1e3, 2e3) to decimal form (1000, 2000). Key finding: oxc-parser returns character offsets, not byte offsets. - Object literal formatting: regex-based collapsing of multi-line simple object literals in function call arguments to single lines. Only targets objects with identifier/member-expression values. Results: numeric diffs 4 → 0, line count diffs 304 → 301, 0 parse errors. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 734aa9c commit 23f21f2

1 file changed

Lines changed: 123 additions & 2 deletions

File tree

napi/angular-compiler/e2e/compare/src/compare.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,15 @@ export async function compareJsSemantically(
14781478
workingTsCode = normalizeNullishCoalescing(workingTsCode)
14791479
workingOxcCode = removeNullishCoalescingParens(workingOxcCode)
14801480

1481-
// Fast path after template/inject/nullish normalization
1481+
// Normalize numeric literals: 1e3 → 1000 (OXC emits scientific notation)
1482+
workingOxcCode = normalizeNumericLiterals(workingOxcCode)
1483+
workingTsCode = normalizeNumericLiterals(workingTsCode)
1484+
1485+
// Normalize inline object literal formatting: collapse multi-line simple objects
1486+
workingOxcCode = collapseInlineObjectLiterals(workingOxcCode)
1487+
workingTsCode = collapseInlineObjectLiterals(workingTsCode)
1488+
1489+
// Fast path after template/inject/nullish/format normalization
14821490
if (workingOxcCode === workingTsCode) {
14831491
return { match: true }
14841492
}
@@ -2906,7 +2914,15 @@ export async function compareFullFileSemantically(
29062914
workingTsCode = normalizeNullishCoalescing(workingTsCode)
29072915
workingOxcCode = removeNullishCoalescingParens(workingOxcCode)
29082916

2909-
// Fast path after template/inject/nullish normalization
2917+
// Normalize numeric literals: 1e3 → 1000 (OXC emits scientific notation)
2918+
workingOxcCode = normalizeNumericLiterals(workingOxcCode)
2919+
workingTsCode = normalizeNumericLiterals(workingTsCode)
2920+
2921+
// Normalize inline object literal formatting: collapse multi-line simple objects
2922+
workingOxcCode = collapseInlineObjectLiterals(workingOxcCode)
2923+
workingTsCode = collapseInlineObjectLiterals(workingTsCode)
2924+
2925+
// Fast path after template/inject/nullish/format normalization
29102926
if (workingOxcCode === workingTsCode) {
29112927
return { match: true }
29122928
}
@@ -3213,3 +3229,108 @@ async function formatCodeForComparison(code: string): Promise<string> {
32133229
return code
32143230
}
32153231
}
3232+
3233+
/**
3234+
* AST node type used for traversal in normalization functions.
3235+
*/
3236+
interface NormAstNode {
3237+
type?: string
3238+
value?: unknown
3239+
start?: number
3240+
end?: number
3241+
[key: string]: unknown
3242+
}
3243+
3244+
/**
3245+
* Recursively walk an AST node, calling the visitor for each object node.
3246+
*/
3247+
function walkAst(node: unknown, visitor: (n: NormAstNode) => void): void {
3248+
if (node === null || typeof node !== 'object') return
3249+
if (Array.isArray(node)) {
3250+
for (const item of node) walkAst(item, visitor)
3251+
return
3252+
}
3253+
const obj = node as NormAstNode
3254+
visitor(obj)
3255+
for (const value of Object.values(obj)) {
3256+
if (value !== null && typeof value === 'object') {
3257+
walkAst(value, visitor)
3258+
}
3259+
}
3260+
}
3261+
3262+
/**
3263+
* Normalize numeric scientific notation to decimal form using oxc-parser.
3264+
* Finds Literal nodes (ESTree format) whose source text contains scientific notation
3265+
* (e.g. `1e3`) and replaces them with their decimal representation (`1000`).
3266+
*/
3267+
function normalizeNumericLiterals(code: string): string {
3268+
let ast
3269+
try {
3270+
ast = parseSync('numeric.js', code, { sourceType: 'module' })
3271+
} catch {
3272+
return code
3273+
}
3274+
3275+
const replacements: Array<{ start: number; end: number; replacement: string }> = []
3276+
3277+
// oxc-parser returns character offsets (not byte offsets), so use start/end directly
3278+
walkAst(ast.program, (node) => {
3279+
if (node.type === 'Literal' && typeof node.value === 'number') {
3280+
if (typeof node.start !== 'number' || typeof node.end !== 'number') return
3281+
const originalText = code.slice(node.start as number, node.end as number)
3282+
// Check if the source text uses scientific notation (exclude hex/octal/binary prefixes)
3283+
if (/e\+?\d/i.test(originalText) && !/^0[xob]/i.test(originalText)) {
3284+
const num = node.value as number
3285+
if (Number.isFinite(num) && Number.isSafeInteger(num)) {
3286+
replacements.push({
3287+
start: node.start as number,
3288+
end: node.end as number,
3289+
replacement: String(num),
3290+
})
3291+
}
3292+
}
3293+
}
3294+
})
3295+
3296+
if (replacements.length === 0) return code
3297+
3298+
// Apply replacements from end to start
3299+
replacements.sort((a, b) => b.start - a.start)
3300+
let result = code
3301+
for (const { start, end, replacement } of replacements) {
3302+
result = result.slice(0, start) + replacement + result.slice(end)
3303+
}
3304+
return result
3305+
}
3306+
3307+
/**
3308+
* Collapse multi-line simple object literals in function call arguments to single lines.
3309+
*
3310+
* Matches patterns like:
3311+
* .emit({
3312+
* content: $event,
3313+
* format: item_r3.format,
3314+
* type: item_r3.id,
3315+
* })
3316+
*
3317+
* And collapses to:
3318+
* .emit({ content: $event, format: item_r3.format, type: item_r3.id})
3319+
*
3320+
* Only targets objects where every property value is a simple expression
3321+
* (identifiers, member access, $event) — no strings, nested objects, or calls.
3322+
*/
3323+
const COLLAPSE_OBJ_RE = /\(\{\s*\n((?:\s*\w+:\s*[\w.$]+,?\s*\n)+)\s*\}\)/g
3324+
3325+
function collapseInlineObjectLiterals(code: string): string {
3326+
return code.replace(COLLAPSE_OBJ_RE, (_match, propsBlock: string) => {
3327+
const lines = propsBlock.trim().split('\n')
3328+
const props = lines.map((l) => l.trim()).filter(Boolean)
3329+
// Remove trailing comma from last property
3330+
const last = props.length - 1
3331+
if (last >= 0 && props[last].endsWith(',')) {
3332+
props[last] = props[last].slice(0, -1)
3333+
}
3334+
return '({ ' + props.join(' ') + '})'
3335+
})
3336+
}

0 commit comments

Comments
 (0)