diff --git a/packages/oxc-unshadowed-visitor/src/bindingNames.test.ts b/packages/oxc-unshadowed-visitor/src/bindingNames.test.ts new file mode 100644 index 0000000..2130b56 --- /dev/null +++ b/packages/oxc-unshadowed-visitor/src/bindingNames.test.ts @@ -0,0 +1,79 @@ +import { describe, test, expect } from 'vitest' +import { parseSync, type ESTree } from 'rolldown/utils' +import { extractBindingNames } from './bindingNames.ts' + +function parse(code: string) { + return parseSync('test.js', code).program +} + +describe('extractBindingNames', () => { + function extractFromParam(code: string): string[] { + const program = parse(`function f(${code}) {}`) + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const fn = program.body[0] as ESTree.Function + const names: string[] = [] + for (const param of fn.params) { + extractBindingNames(param, names) + } + return names + } + + function extractFromDecl(code: string): string[] { + const program = parse(code) + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const decl = program.body[0] as ESTree.VariableDeclaration + const names: string[] = [] + for (const declarator of decl.declarations) { + extractBindingNames(declarator.id, names) + } + return names + } + + test('simple identifier', () => { + expect(extractFromParam('a')).toEqual(['a']) + }) + + test('multiple params', () => { + expect(extractFromParam('a, b, c')).toEqual(['a', 'b', 'c']) + }) + + test('rest param', () => { + expect(extractFromParam('a, ...rest')).toEqual(['a', 'rest']) + }) + + test('array destructuring', () => { + expect(extractFromDecl('const [a, b] = arr')).toEqual(['a', 'b']) + }) + + test('array destructuring with holes', () => { + expect(extractFromDecl('const [a, , b] = arr')).toEqual(['a', 'b']) + }) + + test('object destructuring', () => { + expect(extractFromDecl('const { a, b } = obj')).toEqual(['a', 'b']) + }) + + test('renamed object destructuring', () => { + expect(extractFromDecl('const { x: a, y: b } = obj')).toEqual(['a', 'b']) + }) + + test('rest element in array', () => { + expect(extractFromDecl('const [a, ...rest] = arr')).toEqual(['a', 'rest']) + }) + + test('rest element in object', () => { + expect(extractFromDecl('const { a, ...rest } = obj')).toEqual(['a', 'rest']) + }) + + test('assignment pattern', () => { + expect(extractFromParam('a = 1, b = 2')).toEqual(['a', 'b']) + }) + + test('nested destructuring', () => { + expect(extractFromDecl('const { a: { b, c }, d } = obj')).toEqual(['b', 'c', 'd']) + }) + + test('deeply nested mixed destructuring', () => { + expect(extractFromDecl('const { a: [b, { c: d }], ...e } = obj')).toEqual(['b', 'd', 'e']) + }) +}) diff --git a/packages/oxc-unshadowed-visitor/src/mergeVisitors.test.ts b/packages/oxc-unshadowed-visitor/src/mergeVisitors.test.ts new file mode 100644 index 0000000..e37c595 --- /dev/null +++ b/packages/oxc-unshadowed-visitor/src/mergeVisitors.test.ts @@ -0,0 +1,107 @@ +// oxlint-disable unicorn/consistent-function-scoping +import { describe, test, expect } from 'vitest' +import { mergeVisitors } from './mergeVisitors.ts' +import type { VisitorContext } from './types.ts' +import type { ESTree } from 'rolldown/utils' + +describe('mergeVisitors', () => { + const dummyIdentifierNode: ESTree.IdentifierReference = { + type: 'Identifier', + name: 'foo', + start: 0, + end: 3, + } + function makeCtx(): VisitorContext { + return { + record() {}, + } + } + + test('user enter runs after internal enter', () => { + const order: string[] = [] + const merged = mergeVisitors( + { + Identifier: () => order.push('user-enter'), + }, + makeCtx(), + { Identifier: () => order.push('internal-enter') }, + {}, + ) + merged.Identifier(dummyIdentifierNode) + expect(order).toEqual(['internal-enter', 'user-enter']) + }) + + test('user exit runs before internal exit', () => { + const order: string[] = [] + const merged = mergeVisitors( + { + 'Identifier:exit': () => order.push('user-exit'), + }, + makeCtx(), + {}, + { 'Identifier:exit': () => order.push('internal-exit') }, + ) + merged['Identifier:exit'](dummyIdentifierNode) + expect(order).toEqual(['user-exit', 'internal-exit']) + }) + + test('internal-only visitors are included', () => { + const ctx = makeCtx() + const enterFn = () => {} + const exitFn = () => {} + const merged = mergeVisitors({}, ctx, { Identifier: enterFn }, { 'Identifier:exit': exitFn }) + expect(merged.Identifier).toBe(enterFn) + expect(merged['Identifier:exit']).toBe(exitFn) + }) + + test('user-only visitors are included', () => { + const called: string[] = [] + const ctx = makeCtx() + const merged = mergeVisitors( + { + Identifier: () => called.push('identifier'), + 'Identifier:exit': () => called.push('identifier-exit'), + }, + ctx, + {}, + {}, + ) + merged.Identifier(dummyIdentifierNode) + merged['Identifier:exit'](dummyIdentifierNode) + expect(called).toEqual(['identifier', 'identifier-exit']) + }) + + test('ctx is passed to user visitor functions', () => { + let receivedCtx: unknown + const ctx = makeCtx() + const merged = mergeVisitors( + { + Identifier: (_node, c) => { + receivedCtx = c + }, + }, + ctx, + {}, + {}, + ) + merged.Identifier(dummyIdentifierNode) + expect(receivedCtx).toBe(ctx) + }) + + test('ctx is passed to user exit visitor functions', () => { + let receivedCtx: unknown + const ctx = makeCtx() + const merged = mergeVisitors( + { + 'Identifier:exit': (_node, c) => { + receivedCtx = c + }, + }, + ctx, + {}, + {}, + ) + merged['Identifier:exit'](dummyIdentifierNode) + expect(receivedCtx).toBe(ctx) + }) +})