// ----------------------------------------------------------------------------- // index.test.js // Sprint: SPRINT_3600_0004_0001 (Node.js Babel Integration) // Tasks: NODE-017, NODE-018 - Unit tests for AST parsing and entrypoint detection // Description: Tests for call graph extraction from JavaScript/TypeScript. // ----------------------------------------------------------------------------- import { test, describe, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; // Test utilities for AST parsing function parseCode(code, options = {}) { return parse(code, { sourceType: 'module', plugins: [ 'typescript', 'jsx', 'decorators-legacy', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'dynamicImport', 'optionalChaining', 'nullishCoalescingOperator' ], errorRecovery: true, ...options }); } describe('Babel Parser Integration', () => { test('parses simple JavaScript function', () => { const code = ` function hello(name) { return 'Hello, ' + name; } `; const ast = parseCode(code); assert.ok(ast); assert.equal(ast.type, 'File'); assert.ok(ast.program.body.length > 0); }); test('parses arrow function', () => { const code = ` const greet = (name) => { return \`Hello, \${name}\`; }; `; const ast = parseCode(code); assert.ok(ast); let foundArrow = false; traverse.default(ast, { ArrowFunctionExpression() { foundArrow = true; } }); assert.ok(foundArrow, 'Should find arrow function'); }); test('parses async function', () => { const code = ` async function fetchData(url) { const response = await fetch(url); return response.json(); } `; const ast = parseCode(code); let isAsync = false; traverse.default(ast, { FunctionDeclaration(path) { isAsync = path.node.async; } }); assert.ok(isAsync, 'Should detect async function'); }); test('parses class with methods', () => { const code = ` class UserController { async getUser(id) { return this.userService.findById(id); } async createUser(data) { return this.userService.create(data); } } `; const ast = parseCode(code); const methods = []; traverse.default(ast, { ClassMethod(path) { methods.push(path.node.key.name); } }); assert.deepEqual(methods.sort(), ['createUser', 'getUser']); }); test('parses TypeScript with types', () => { const code = ` interface User { id: string; name: string; } function getUser(id: string): Promise { return db.query('SELECT * FROM users WHERE id = $1', [id]); } `; const ast = parseCode(code); assert.ok(ast); let foundFunction = false; traverse.default(ast, { FunctionDeclaration(path) { if (path.node.id.name === 'getUser') { foundFunction = true; } } }); assert.ok(foundFunction, 'Should parse TypeScript function'); }); test('parses JSX components', () => { const code = ` function Button({ onClick, children }) { return ; } `; const ast = parseCode(code); let foundJSX = false; traverse.default(ast, { JSXElement() { foundJSX = true; } }); assert.ok(foundJSX, 'Should parse JSX'); }); test('parses decorators', () => { const code = ` @Controller('/users') class UserController { @Get('/:id') async getUser(@Param('id') id: string) { return this.userService.findById(id); } } `; const ast = parseCode(code); const decorators = []; traverse.default(ast, { ClassDeclaration(path) { if (path.node.decorators) { decorators.push(...path.node.decorators.map(d => d.expression?.callee?.name || d.expression?.name )); } }, ClassMethod(path) { if (path.node.decorators) { decorators.push(...path.node.decorators.map(d => d.expression?.callee?.name || d.expression?.name )); } } }); assert.ok(decorators.includes('Controller')); assert.ok(decorators.includes('Get')); }); test('parses dynamic imports', () => { const code = ` async function loadModule(name) { const module = await import(\`./modules/\${name}\`); return module.default; } `; const ast = parseCode(code); let foundDynamicImport = false; traverse.default(ast, { Import() { foundDynamicImport = true; } }); assert.ok(foundDynamicImport, 'Should detect dynamic import'); }); test('parses optional chaining', () => { const code = ` const name = user?.profile?.name ?? 'Anonymous'; `; const ast = parseCode(code); let foundOptionalChain = false; traverse.default(ast, { OptionalMemberExpression() { foundOptionalChain = true; } }); assert.ok(foundOptionalChain, 'Should parse optional chaining'); }); test('parses class private fields', () => { const code = ` class Counter { #count = 0; increment() { this.#count++; } get value() { return this.#count; } } `; const ast = parseCode(code); let foundPrivateField = false; traverse.default(ast, { ClassPrivateProperty() { foundPrivateField = true; } }); assert.ok(foundPrivateField, 'Should parse private class field'); }); }); describe('Function Declaration Extraction', () => { test('extracts function name', () => { const code = ` function processRequest(req, res) { res.json({ status: 'ok' }); } `; const ast = parseCode(code); let functionName = null; traverse.default(ast, { FunctionDeclaration(path) { functionName = path.node.id.name; } }); assert.equal(functionName, 'processRequest'); }); test('extracts function parameters', () => { const code = ` function greet(firstName, lastName, options = {}) { return \`Hello, \${firstName} \${lastName}\`; } `; const ast = parseCode(code); let params = []; traverse.default(ast, { FunctionDeclaration(path) { params = path.node.params.map(p => { if (p.type === 'Identifier') return p.name; if (p.type === 'AssignmentPattern') return p.left.name; return 'unknown'; }); } }); assert.deepEqual(params, ['firstName', 'lastName', 'options']); }); test('detects exported functions', () => { const code = ` export function publicFunction() {} function privateFunction() {} export default function defaultFunction() {} `; const ast = parseCode(code); const functions = { public: [], private: [] }; traverse.default(ast, { FunctionDeclaration(path) { const name = path.node.id?.name; if (!name) return; const isExported = path.parent.type === 'ExportNamedDeclaration' || path.parent.type === 'ExportDefaultDeclaration'; if (isExported) { functions.public.push(name); } else { functions.private.push(name); } } }); assert.deepEqual(functions.public.sort(), ['defaultFunction', 'publicFunction']); assert.deepEqual(functions.private, ['privateFunction']); }); }); describe('Call Expression Extraction', () => { test('extracts direct function calls', () => { const code = ` function main() { helper(); processData(); } `; const ast = parseCode(code); const calls = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'Identifier') { calls.push(path.node.callee.name); } } }); assert.deepEqual(calls.sort(), ['helper', 'processData']); }); test('extracts method calls', () => { const code = ` function handler() { db.query('SELECT * FROM users'); fs.readFile('./config.json'); console.log('done'); } `; const ast = parseCode(code); const methodCalls = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const obj = path.node.callee.object.name; const method = path.node.callee.property.name; methodCalls.push(`${obj}.${method}`); } } }); assert.ok(methodCalls.includes('db.query')); assert.ok(methodCalls.includes('fs.readFile')); assert.ok(methodCalls.includes('console.log')); }); test('extracts chained method calls', () => { const code = ` const result = data .filter(x => x.active) .map(x => x.name) .join(', '); `; const ast = parseCode(code); const methods = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const method = path.node.callee.property.name; methods.push(method); } } }); assert.ok(methods.includes('filter')); assert.ok(methods.includes('map')); assert.ok(methods.includes('join')); }); }); describe('Framework Entrypoint Detection', () => { test('detects Express route handlers', () => { const code = ` const express = require('express'); const app = express(); app.get('/users', (req, res) => { res.json(users); }); app.post('/users', async (req, res) => { const user = await createUser(req.body); res.json(user); }); app.delete('/users/:id', (req, res) => { deleteUser(req.params.id); res.sendStatus(204); }); `; const ast = parseCode(code); const routes = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const method = path.node.callee.property.name?.toLowerCase(); const httpMethods = ['get', 'post', 'put', 'delete', 'patch']; if (httpMethods.includes(method)) { const routeArg = path.node.arguments[0]; if (routeArg?.type === 'StringLiteral') { routes.push({ method: method.toUpperCase(), path: routeArg.value }); } } } } }); assert.equal(routes.length, 3); assert.ok(routes.some(r => r.method === 'GET' && r.path === '/users')); assert.ok(routes.some(r => r.method === 'POST' && r.path === '/users')); assert.ok(routes.some(r => r.method === 'DELETE' && r.path === '/users/:id')); }); test('detects Fastify route handlers', () => { const code = ` const fastify = require('fastify')(); fastify.get('/health', async (request, reply) => { return { status: 'ok' }; }); fastify.route({ method: 'POST', url: '/items', handler: async (request, reply) => { return { id: 1 }; } }); `; const ast = parseCode(code); const routes = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const method = path.node.callee.property.name?.toLowerCase(); if (['get', 'post', 'put', 'delete', 'patch', 'route'].includes(method)) { const routeArg = path.node.arguments[0]; if (routeArg?.type === 'StringLiteral') { routes.push({ method: method.toUpperCase(), path: routeArg.value }); } } } } }); assert.ok(routes.some(r => r.path === '/health')); }); test('detects NestJS controller decorators', () => { const code = ` @Controller('users') export class UsersController { @Get() findAll() { return this.usersService.findAll(); } @Get(':id') findOne(@Param('id') id: string) { return this.usersService.findOne(id); } @Post() create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } } `; const ast = parseCode(code); const handlers = []; traverse.default(ast, { ClassMethod(path) { const decorators = path.node.decorators || []; for (const decorator of decorators) { const name = decorator.expression?.callee?.name || decorator.expression?.name; if (['Get', 'Post', 'Put', 'Delete', 'Patch'].includes(name)) { handlers.push({ method: name.toUpperCase(), handler: path.node.key.name }); } } } }); assert.equal(handlers.length, 3); assert.ok(handlers.some(h => h.handler === 'findAll')); assert.ok(handlers.some(h => h.handler === 'findOne')); assert.ok(handlers.some(h => h.handler === 'create')); }); test('detects Koa router handlers', () => { const code = ` const Router = require('koa-router'); const router = new Router(); router.get('/items', async (ctx) => { ctx.body = await getItems(); }); router.post('/items', async (ctx) => { ctx.body = await createItem(ctx.request.body); }); `; const ast = parseCode(code); const routes = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const objName = path.node.callee.object.name; const method = path.node.callee.property.name?.toLowerCase(); if (objName === 'router' && ['get', 'post', 'put', 'delete'].includes(method)) { const routeArg = path.node.arguments[0]; if (routeArg?.type === 'StringLiteral') { routes.push({ method: method.toUpperCase(), path: routeArg.value }); } } } } }); assert.equal(routes.length, 2); assert.ok(routes.some(r => r.method === 'GET' && r.path === '/items')); assert.ok(routes.some(r => r.method === 'POST' && r.path === '/items')); }); test('detects AWS Lambda handlers', () => { const code = ` export const handler = async (event, context) => { const body = JSON.parse(event.body); return { statusCode: 200, body: JSON.stringify({ message: 'Success' }) }; }; export const main = async (event) => { return { statusCode: 200 }; }; `; const ast = parseCode(code); const handlers = []; traverse.default(ast, { VariableDeclarator(path) { const name = path.node.id?.name?.toLowerCase(); if (['handler', 'main'].includes(name)) { if (path.node.init?.type === 'ArrowFunctionExpression') { handlers.push(path.node.id.name); } } } }); assert.ok(handlers.includes('handler')); assert.ok(handlers.includes('main')); }); test('detects Hapi route handlers', () => { const code = ` const server = Hapi.server({ port: 3000 }); server.route({ method: 'GET', path: '/users', handler: (request, h) => { return getUsers(); } }); server.route({ method: 'POST', path: '/users', handler: async (request, h) => { return createUser(request.payload); } }); `; const ast = parseCode(code); let routeCount = 0; traverse.default(ast, { CallExpression(path) { if (path.node.callee.type === 'MemberExpression') { const method = path.node.callee.property.name; if (method === 'route') { routeCount++; } } } }); assert.equal(routeCount, 2); }); }); describe('Module Import/Export Detection', () => { test('detects CommonJS require', () => { const code = ` const express = require('express'); const { Router } = require('express'); const db = require('./db'); `; const ast = parseCode(code); const imports = []; traverse.default(ast, { CallExpression(path) { if (path.node.callee.name === 'require') { const arg = path.node.arguments[0]; if (arg?.type === 'StringLiteral') { imports.push(arg.value); } } } }); assert.ok(imports.includes('express')); assert.ok(imports.includes('./db')); }); test('detects ES module imports', () => { const code = ` import express from 'express'; import { Router, Request, Response } from 'express'; import * as fs from 'fs'; import db from './db.js'; `; const ast = parseCode(code); const imports = []; traverse.default(ast, { ImportDeclaration(path) { imports.push(path.node.source.value); } }); assert.ok(imports.includes('express')); assert.ok(imports.includes('fs')); assert.ok(imports.includes('./db.js')); }); test('detects ES module exports', () => { const code = ` export function publicFn() {} export const publicConst = 42; export default class MainClass {} export { helper, utils }; `; const ast = parseCode(code); let exportCount = 0; traverse.default(ast, { ExportNamedDeclaration() { exportCount++; }, ExportDefaultDeclaration() { exportCount++; } }); assert.ok(exportCount >= 3); }); });