nerdamer-prime
Version:
javascript light-weight symbolic math library
1,063 lines (911 loc) • 58.2 kB
text/typescript
import type nerdamer from '../index';
import tsMorph from 'ts-morph';
// Type alias for dynamic property access in introspection tests
type Indexable = Record<string, any>;
describe('Nerdamer AST Introspection Tests', () => {
let project: tsMorph.Project;
let sourceFile: tsMorph.SourceFile;
let nerdamerRuntime: typeof nerdamer & Indexable;
beforeAll(async () => {
// Load complete nerdamer with all modules using dynamic import
const module = await import('../all.js');
nerdamerRuntime = module.default || module;
project = new tsMorph.Project({
tsConfigFilePath: 'tsconfig.json',
});
sourceFile = project.getSourceFileOrThrow('index.d.ts');
});
it('should validate all method return types match TypeScript declarations using AST introspection', () => {
const typeValidationErrors: string[] = [];
const methodAnalysis: Record<string, any> = {};
// Helper function to extract return type from TypeScript signature
function extractReturnType(methodDeclaration: tsMorph.MethodSignature | tsMorph.PropertySignature): string {
if (!('getReturnTypeNode' in methodDeclaration)) {
return 'unknown';
}
const returnTypeNode = methodDeclaration.getReturnTypeNode();
if (!returnTypeNode) {
return 'void';
}
const typeText = returnTypeNode.getText();
return typeText;
}
// Helper function to get method parameters
function getMethodParameters(
methodDeclaration: tsMorph.MethodSignature | tsMorph.PropertySignature
): { name: string; type: string; optional: boolean }[] {
if (!('getParameters' in methodDeclaration)) {
return [];
}
const params = methodDeclaration.getParameters();
return params.map((param: tsMorph.ParameterDeclaration) => ({
name: param.getName(),
type: param.getTypeNode()?.getText() || 'unknown',
optional: param.hasQuestionToken(),
}));
}
// Helper function to validate runtime type against declared type
function validateReturnType(actualValue: any, declaredType: string, _methodName: string): boolean {
const actualType = typeof actualValue;
// Handle specific type mappings
const typeMap: Record<string, (value: any) => boolean> = {
boolean: v => typeof v === 'boolean',
string: v => typeof v === 'string',
number: v => typeof v === 'number',
'string[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'string'),
'number[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'number'),
NerdamerExpression: v =>
v !== null &&
typeof v === 'object' &&
typeof v.toString === 'function' &&
typeof v.text === 'function',
'NerdamerExpression[]': v =>
Array.isArray(v) &&
v.every(
(item: unknown) =>
item !== null &&
typeof item === 'object' &&
typeof (item as Record<string, unknown>).toString === 'function'
),
void: v => v === undefined,
};
// Check for array types FIRST (before checking for single types)
if (declaredType === 'NerdamerExpression[]') {
const validator = typeMap['NerdamerExpression[]'];
return validator ? validator(actualValue) : false;
}
// Check for generic NerdamerExpression types
if (declaredType.includes('NerdamerExpression')) {
const validator = typeMap['NerdamerExpression'];
return validator ? validator(actualValue) : false;
}
// Check exact type match
const validator = typeMap[declaredType];
if (validator) {
return validator(actualValue);
}
// Fallback: basic type checking
return actualType !== 'undefined';
}
// Helper function to generate test arguments for methods
function generateTestArguments(parameters: { name: string; type: string; optional: boolean }[]): unknown[] {
const testExpr = nerdamerRuntime('x + 1');
const args: unknown[] = [];
for (const param of parameters) {
if (param.optional) {
break;
} // Stop at first optional parameter for simplicity
// Generate appropriate test values based on parameter type
if (param.type.includes('ExpressionParam')) {
args.push(testExpr);
} else if (param.type === 'string') {
args.push('x');
} else if (param.type === 'number') {
args.push(2);
} else if (param.type === 'boolean') {
args.push(true);
} else if (param.type.includes('Record<string,')) {
args.push({ x: 1 });
} else {
args.push(testExpr); // Default fallback
}
}
return args;
}
try {
// Find NerdamerExpression interface
const nerdamerExpressionInterface = sourceFile
.getDescendantsOfKind(tsMorph.SyntaxKind.InterfaceDeclaration)
.find((iface: tsMorph.InterfaceDeclaration) => iface.getName() === 'NerdamerExpression');
if (!nerdamerExpressionInterface) {
typeValidationErrors.push('NerdamerExpression interface not found in type definitions');
expect(typeValidationErrors).toEqual([]);
return;
}
// Get all method signatures from the interface
const methodSignatures = nerdamerExpressionInterface.getMethods();
console.log(`\n=== Found ${methodSignatures.length} method signatures in NerdamerExpression interface ===`);
// Create test instances
const testExpressions = [
nerdamerRuntime('x + 1'),
nerdamerRuntime('x^2 + 2*x + 1'),
nerdamerRuntime('sin(x)'),
nerdamerRuntime('2'),
nerdamerRuntime('x'),
];
for (const methodSig of methodSignatures) {
const methodName = methodSig.getName();
const returnType = extractReturnType(methodSig);
const parameters = getMethodParameters(methodSig);
methodAnalysis[methodName] = {
returnType,
parameters: parameters.length,
tested: false,
errors: [],
};
console.log(`Testing method: ${methodName}() -> ${returnType}`);
// Test method on each test expression
for (let i = 0; i < testExpressions.length; i++) {
const testExpr = testExpressions[i] as Indexable;
if (typeof testExpr[methodName] !== 'function') {
methodAnalysis[methodName].errors.push(`Method ${methodName} not found on runtime object`);
continue;
}
try {
// Generate appropriate arguments
const args = generateTestArguments(parameters);
// Call the method
const result = testExpr[methodName](...args);
// Validate return type
const isValidType = validateReturnType(result, returnType, methodName);
if (!isValidType) {
const errorMsg = `${methodName}() declared to return '${returnType}' but returned '${typeof result}': ${JSON.stringify(result).substring(0, 100)}`;
methodAnalysis[methodName].errors.push(errorMsg);
typeValidationErrors.push(errorMsg);
}
methodAnalysis[methodName].tested = true;
// Break after first successful test to avoid redundancy
break;
} catch (error) {
// Some methods may throw for certain inputs, record but continue
const errorMessage = error instanceof Error ? error.message : String(error);
methodAnalysis[methodName].errors.push(`Threw error: ${errorMessage}`);
// Special handling for methods that should never throw
if (['toString', 'variables', 'isNumber', 'equals'].includes(methodName)) {
typeValidationErrors.push(`Critical method ${methodName}() threw error: ${errorMessage}`);
}
}
}
}
// Report analysis
console.log('\n=== Method Analysis Summary ===');
const testedMethods = Object.keys(methodAnalysis).filter(m => methodAnalysis[m].tested);
const errorMethods = Object.keys(methodAnalysis).filter(m => methodAnalysis[m].errors.length > 0);
console.log(`Total methods analyzed: ${Object.keys(methodAnalysis).length}`);
console.log(`Successfully tested: ${testedMethods.length}`);
console.log(`Methods with errors: ${errorMethods.length}`);
if (errorMethods.length > 0) {
console.log('\nMethods with issues:');
errorMethods.forEach(method => {
console.log(` ${method}: ${methodAnalysis[method].errors.join(', ')}`);
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
typeValidationErrors.push(`AST introspection failed: ${errorMessage}`);
}
if (typeValidationErrors.length > 0) {
console.log('\n=== Critical Return Type Validation Errors ===');
typeValidationErrors.forEach(validationError => {
console.log('❌', validationError);
});
}
// This test should fail if there are type mismatches to highlight the issues
expect(typeValidationErrors).toEqual([]);
});
it('should validate nerdamerPrime namespace function signatures using AST introspection', () => {
const primeValidationErrors: string[] = [];
const functionAnalysis: Record<string, any> = {};
// Helper function to extract return type from TypeScript function signature
function extractReturnType(functionDeclaration: tsMorph.FunctionDeclaration): string {
const returnTypeNode = functionDeclaration.getReturnTypeNode();
if (!returnTypeNode) {
return 'void';
}
const typeText = returnTypeNode.getText();
return typeText;
}
// Helper function to get function parameters
function getFunctionParameters(
functionDeclaration: tsMorph.FunctionDeclaration
): { name: string; type: string; optional: boolean; rest: boolean }[] {
const params = functionDeclaration.getParameters();
return params.map((param: tsMorph.ParameterDeclaration) => ({
name: param.getName(),
type: param.getTypeNode()?.getText() || 'unknown',
optional: param.hasQuestionToken(),
rest: param.isRestParameter(),
}));
}
// Helper function to validate runtime type against declared type
function validatePrimeReturnType(actualValue: unknown, declaredType: string, functionName: string): boolean {
const actualType = typeof actualValue;
// Enhanced NerdamerExpression validation that detects object wrappers
function isProperNerdamerExpression(v: unknown): boolean {
if (!v || typeof v !== 'object') {
return false;
}
const obj = v as Record<string, unknown>;
// Must have the required methods
if (typeof obj.toString !== 'function' || typeof obj['text'] !== 'function') {
return false;
}
// Must have a symbol property
if (!('symbol' in v)) {
return false;
}
// Critical check: symbol property should not be undefined for valid expressions
// This detects the vecget issue where invalid indices return { symbol: undefined }
if (obj['symbol'] === undefined) {
return false;
}
// The symbol should be a proper NerdamerSymbol object with expected properties
if (obj['symbol'] && typeof obj['symbol'] === 'object') {
// A proper NerdamerSymbol should have these core properties according to the type definitions
const symbolObj = obj['symbol'] as Record<string, unknown>;
const hasSymbolProps =
'group' in symbolObj &&
'value' in symbolObj &&
'multiplier' in symbolObj &&
'power' in symbolObj;
if (!hasSymbolProps) {
return false;
}
}
return true;
}
// Handle specific type mappings for nerdamerPrime functions
const typeMap: Record<string, (value: unknown) => boolean> = {
string: v => typeof v === 'string',
number: v => typeof v === 'number',
boolean: v => typeof v === 'boolean',
'string[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'string'),
'number[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'number'),
void: v => v === undefined,
NerdamerExpression: isProperNerdamerExpression,
'NerdamerCore.Vector': v => {
const obj = v as Record<string, unknown> | null;
const symbolObj = obj?.['symbol'] as Record<string, unknown> | null;
return (
v !== null &&
typeof v === 'object' &&
typeof obj?.['toString'] === 'function' &&
symbolObj?.['group'] === 8
);
},
'NerdamerCore.Matrix': v => {
const obj = v as Record<string, unknown> | null;
const symbolObj = obj?.['symbol'] as Record<string, unknown> | null;
return (
v !== null &&
typeof v === 'object' &&
typeof obj?.['toString'] === 'function' &&
symbolObj?.['group'] === 8
);
},
'NerdamerCore.Set': v => {
const obj = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && typeof obj?.['toString'] === 'function';
},
NerdamerEquation: v => {
const obj = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && !!obj?.['LHS'] && !!obj?.['RHS'];
},
'typeof nerdamer': v => typeof v === 'function',
};
// Special handling for overloaded functions
if (functionName === 'supported') {
// Supported() with no args returns string[], with string arg returns boolean
if (Array.isArray(actualValue)) {
return declaredType === 'string[]' || (typeMap['string[]']?.(actualValue) ?? false);
}
if (typeof actualValue === 'boolean') {
return declaredType === 'boolean' || (typeMap['boolean']?.(actualValue) ?? false);
}
return false;
}
// Check for Record types first (before NerdamerExpression check)
if (declaredType.startsWith('Record<')) {
return typeof actualValue === 'object' && actualValue !== null;
}
// Check for array types FIRST (before checking for single types)
if (declaredType === 'NerdamerExpression[]') {
return (
Array.isArray(actualValue) &&
actualValue.every(
(item: unknown) =>
item !== null &&
typeof item === 'object' &&
typeof (item as Record<string, unknown>).toString === 'function'
)
);
}
// Check for complex types containing NerdamerExpression or other patterns
if (declaredType.includes('NerdamerExpression')) {
const validator = typeMap['NerdamerExpression'];
return validator ? validator(actualValue) : false;
}
if (declaredType.includes('Vector')) {
const validator = typeMap['NerdamerCore.Vector'];
return validator ? validator(actualValue) : false;
}
if (declaredType.includes('Matrix')) {
const validator = typeMap['NerdamerCore.Matrix'];
return validator ? validator(actualValue) : false;
}
if (declaredType.includes('nerdamer')) {
const validator = typeMap['typeof nerdamer'];
return validator ? validator(actualValue) : false;
}
// Check exact type match
const validator = typeMap[declaredType];
if (validator) {
return validator(actualValue);
}
// Fallback: basic type checking
return actualType !== 'undefined';
}
// Helper function to generate test arguments for nerdamerPrime functions
function generatePrimeTestArguments(
parameters: { name: string; type: string; optional: boolean; rest: boolean }[]
): unknown[] {
const testExpr = nerdamerRuntime('x + 1');
const args: unknown[] = [];
for (const param of parameters) {
if (param.optional) {
break;
} // Stop at first optional parameter for safety
// Generate appropriate test values based on parameter type
if (param.type.includes('ExpressionParam')) {
args.push(testExpr);
} else if (param.type === 'string') {
args.push('x');
} else if (param.type === 'number') {
args.push(2);
} else if (param.type === 'boolean') {
args.push(true);
} else if (param.type.includes('Record<string,')) {
args.push({ x: 1 });
} else if (param.type.includes('[]')) {
if (param.type.includes('ExpressionParam')) {
args.push([testExpr, 'y']);
} else if (param.type.includes('string')) {
args.push(['x', 'y']);
} else {
args.push([1, 2]);
}
} else if (param.rest) {
// For rest parameters, provide multiple arguments
args.push(testExpr, 'y', 2);
break;
} else {
args.push(testExpr); // Default fallback
}
}
return args;
}
try {
// Find nerdamerPrime namespace
const nerdamerPrimeNamespace = sourceFile
.getDescendantsOfKind(tsMorph.SyntaxKind.ModuleDeclaration)
.find((ns: tsMorph.ModuleDeclaration) => ns.getName() === 'nerdamerPrime');
if (!nerdamerPrimeNamespace) {
primeValidationErrors.push('nerdamerPrime namespace not found in type definitions');
expect(primeValidationErrors).toEqual([]);
return;
}
// Get all function declarations from the namespace
const functionDeclarations = nerdamerPrimeNamespace.getDescendantsOfKind(
tsMorph.SyntaxKind.FunctionDeclaration
);
console.log(
`\n=== Found ${functionDeclarations.length} function declarations in nerdamerPrime namespace ===`
);
// Functions that are safe to test (won't cause side effects or require complex setup)
const safeFunctions = new Set([
'version',
'reserved',
'expressions',
'functions',
'getWarnings',
'numExpressions',
'numEquations',
'getVars',
'supported',
'abs',
'floor',
'ceil',
'sqrt',
'exp',
'log',
'sin',
'cos',
'tan',
]);
for (const funcDecl of functionDeclarations) {
const functionName = funcDecl.getName();
// Skip if function name is undefined
if (!functionName) {
console.log('Skipping function declaration without name');
continue;
}
const returnType = extractReturnType(funcDecl);
const parameters = getFunctionParameters(funcDecl);
functionAnalysis[functionName] = {
returnType,
parameters: parameters.length,
parameterInfo: parameters,
tested: false,
errors: [],
};
console.log(
`Testing function: ${functionName}(${parameters.map(p => `${p.name}${p.optional ? '?' : ''}${p.rest ? '...' : ''}: ${p.type}`).join(', ')}) -> ${returnType}`
);
// Check if function exists in runtime
const runtimeFunction = nerdamerRuntime[functionName];
if (typeof runtimeFunction !== 'function') {
functionAnalysis[functionName].errors.push(`Function ${functionName} not found on runtime object`);
primeValidationErrors.push(
`Function ${functionName} declared in nerdamerPrime but not found in runtime`
);
continue;
}
try {
// Only test safe functions to avoid side effects
if (safeFunctions.has(functionName) || parameters.length === 0) {
let result;
if (parameters.length === 0) {
// Functions with no parameters
result = runtimeFunction();
} else {
// Generate appropriate arguments
const args = generatePrimeTestArguments(parameters);
// Limit arguments to prevent excessive calls
const limitedArgs = args.slice(0, Math.min(args.length, 3));
result = runtimeFunction(...limitedArgs);
}
// Validate return type
const isValidType = validatePrimeReturnType(result, returnType, functionName);
if (!isValidType) {
const errorMsg = `${functionName}() declared to return '${returnType}' but returned '${typeof result}': ${JSON.stringify(result).substring(0, 100)}`;
functionAnalysis[functionName].errors.push(errorMsg);
primeValidationErrors.push(errorMsg);
}
functionAnalysis[functionName].tested = true;
} else {
// For potentially unsafe functions, validate arity with more sophistication
const requiredParams = parameters.filter(p => !p.optional && !p.rest).length;
const runtimeArity = runtimeFunction.length;
// Check if function uses arguments object or rest parameters (both are valid patterns)
const functionSource = runtimeFunction.toString();
const usesArguments = functionSource.includes('arguments');
const usesRestParams = /\(\.\.\.|\(\s*\w+\s*,\s*\.\.\./u.test(functionSource);
const usesVariableArgs = usesArguments || usesRestParams;
// Known functions that work correctly despite arity differences
// These use arguments object or have internal params for recursion
const knownWorkingFunctions = new Set([
'matset', // Uses arguments, works with 4 params
'sum', // Uses arguments, works with 4 params
'product', // Uses arguments, works with 4 params
'solveEquations', // Internal recursion params, works with 1-2 params
]);
// Skip validation for known working functions
if (knownWorkingFunctions.has(functionName)) {
functionAnalysis[functionName].arityPattern = 'known-working';
functionAnalysis[functionName].tested = 'arity-analysis';
functionAnalysis[functionName].usesArguments = usesVariableArgs;
functionAnalysis[functionName].runtimeArity = runtimeArity;
functionAnalysis[functionName].requiredParams = requiredParams;
continue;
}
// If function has zero arity but uses arguments or rest params, this is likely intentional
if (runtimeArity === 0 && usesVariableArgs) {
// Try a functional test to see if it actually works
let functionallyWorks = false;
try {
// Generate minimal test arguments
const testArgs: unknown[] = [];
for (let i = 0; i < Math.min(requiredParams, 3); i++) {
if (parameters[i]?.type.includes('string')) {
testArgs.push('x');
} else if (parameters[i]?.type.includes('number')) {
testArgs.push(1);
} else if (parameters[i]?.type.includes('ExpressionParam')) {
testArgs.push('x');
} else {
testArgs.push('x'); // Fallback
}
}
// Test if function works with expected arguments
const result = runtimeFunction(...testArgs);
functionallyWorks = result !== undefined;
functionAnalysis[functionName].arityPattern = 'arguments-object-functional';
} catch (error) {
// Function might throw for test arguments, but that's not necessarily an arity issue
functionAnalysis[functionName].arityPattern = 'arguments-object-throws';
}
// Only flag as error if function doesn't work functionally AND there's a significant mismatch
if (!functionallyWorks && requiredParams > 3) {
const arityError = `${functionName}: Uses arguments object but may not handle ${requiredParams} parameters correctly`;
functionAnalysis[functionName].errors.push(arityError);
primeValidationErrors.push(arityError);
}
} else if (runtimeArity > 0 && Math.abs(runtimeArity - requiredParams) > 2) {
// Traditional declared parameters with significant mismatch
const arityError = `${functionName}: Runtime arity ${runtimeArity} vs required parameters ${requiredParams}`;
functionAnalysis[functionName].errors.push(arityError);
primeValidationErrors.push(arityError);
} else if (runtimeArity === 0 && !usesVariableArgs && requiredParams > 0) {
// Zero arity without arguments object or rest params usage - potentially problematic
const arityError = `${functionName}: Zero arity but requires ${requiredParams} parameters and doesn't use arguments object or rest parameters`;
functionAnalysis[functionName].errors.push(arityError);
primeValidationErrors.push(arityError);
}
functionAnalysis[functionName].tested = 'arity-analysis';
functionAnalysis[functionName].usesArguments = usesVariableArgs;
functionAnalysis[functionName].runtimeArity = runtimeArity;
functionAnalysis[functionName].requiredParams = requiredParams;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
functionAnalysis[functionName].errors.push(`Threw error: ${errorMessage}`);
// Special handling for critical functions that should never throw
if (['version', 'reserved', 'functions', 'getWarnings'].includes(functionName)) {
primeValidationErrors.push(`Critical function ${functionName}() threw error: ${errorMessage}`);
}
}
}
// Report analysis
console.log('\n=== nerdamerPrime Function Analysis Summary ===');
const testedFunctions = Object.keys(functionAnalysis).filter(f => functionAnalysis[f].tested === true);
const arityAnalyzedFunctions = Object.keys(functionAnalysis).filter(
f => functionAnalysis[f].tested === 'arity-analysis'
);
const errorFunctions = Object.keys(functionAnalysis).filter(f => functionAnalysis[f].errors.length > 0);
// Count arity patterns
const arityPatterns = {
argumentsObject: 0,
traditionalParams: 0,
zeroArity: 0,
functional: 0,
};
for (const funcName of Object.keys(functionAnalysis)) {
const analysis = functionAnalysis[funcName];
if (analysis.usesArguments !== undefined) {
if (analysis.usesArguments) {
arityPatterns.argumentsObject++;
if (analysis.arityPattern === 'arguments-object-functional') {
arityPatterns.functional++;
}
} else if (analysis.runtimeArity === 0) {
arityPatterns.zeroArity++;
} else {
arityPatterns.traditionalParams++;
}
}
}
console.log(`Total functions analyzed: ${Object.keys(functionAnalysis).length}`);
console.log(`Functions fully tested: ${testedFunctions.length}`);
console.log(`Functions with arity analysis: ${arityAnalyzedFunctions.length}`);
console.log(`Functions with errors: ${errorFunctions.length}`);
console.log('\nArity Pattern Distribution:');
console.log(` Uses arguments object: ${arityPatterns.argumentsObject}`);
console.log(` Arguments object + functional: ${arityPatterns.functional}`);
console.log(` Traditional parameters: ${arityPatterns.traditionalParams}`);
console.log(` Zero arity (no arguments usage): ${arityPatterns.zeroArity}`);
if (errorFunctions.length > 0) {
console.log('\nFunctions with issues (first 10):');
errorFunctions.slice(0, 10).forEach(funcName => {
console.log(` ${funcName}: ${functionAnalysis[funcName].errors.join(', ')}`);
});
}
// Validate function categories exist
const expectedCategories = {
core: ['version', 'getCore', 'clear', 'flush'],
math: ['abs', 'sqrt', 'exp', 'log', 'sin', 'cos', 'tan'],
algebra: ['expand', 'factor', 'simplify', 'solve'],
calculus: ['diff', 'integrate', 'defint'],
matrix: ['matrix', 'vector', 'determinant'],
};
let totalExpected = 0;
let totalFound = 0;
for (const [category, funcs] of Object.entries(expectedCategories)) {
const foundCount = funcs.filter(f => functionAnalysis[f]).length;
totalExpected += funcs.length;
totalFound += foundCount;
console.log(`${category} functions: ${foundCount}/${funcs.length} found`);
}
console.log(
`Expected key functions coverage: ${totalFound}/${totalExpected} (${Math.round((totalFound / totalExpected) * 100)}%)`
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
primeValidationErrors.push(`nerdamerPrime AST introspection failed: ${errorMessage}`);
}
if (primeValidationErrors.length > 0) {
console.log('\n=== Critical nerdamerPrime Function Validation Errors ===');
primeValidationErrors.forEach(validationError => {
console.log('❌', validationError);
});
}
// This test should highlight issues in the nerdamerPrime namespace
expect(primeValidationErrors).toEqual([]);
});
it('should validate functions using metadata-enhanced testing', () => {
const metadataValidationErrors: string[] = [];
const functionAnalysis: Record<string, any> = {};
// Helper function to extract JSDoc comments and parse test metadata
function extractTestMetadata(functionDeclaration: tsMorph.FunctionDeclaration): {
validArgs?: unknown[];
invalidArgs?: unknown[];
description?: string;
} {
const jsDocComments = functionDeclaration.getJsDocs();
const metadata: {
validArgs?: unknown[];
invalidArgs?: unknown[];
description?: string;
} = {};
for (const jsDoc of jsDocComments) {
// Use getFullText() instead of getComment() to get the complete JSDoc including custom tags
const fullText = jsDoc.getFullText();
if (typeof fullText === 'string') {
// Parse @test-valid-args
const validArgsMatch = /@test-valid-args\s*(?<args>\[.*?\])/u.exec(fullText);
if (validArgsMatch?.groups?.['args']) {
try {
metadata.validArgs = JSON.parse(validArgsMatch.groups['args']) as unknown[];
} catch (parseError) {
console.warn(`Failed to parse @test-valid-args for function: ${parseError}`);
}
}
// Parse @test-invalid-args
const invalidArgsMatch = /@test-invalid-args\s*(?<args>\[.*?\])/u.exec(fullText);
if (invalidArgsMatch?.groups?.['args']) {
try {
metadata.invalidArgs = JSON.parse(invalidArgsMatch.groups['args']) as unknown[];
} catch (parseError) {
console.warn(`Failed to parse @test-invalid-args for function: ${parseError}`);
}
}
}
}
return metadata;
}
// Helper function to evaluate test arguments in the test context
function evaluateTestArgs(args: unknown[]): unknown[] {
return args.map(arg => {
if (typeof arg === 'string' && arg.includes('nerdamer.')) {
// Evaluate nerdamer expressions by replacing nerdamer. with nerdamerRuntime.
try {
const evaluatedArg = arg.replace(/nerdamer\./gu, 'nerdamerRuntime.');
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
return new Function('nerdamerRuntime', `return ${evaluatedArg}`)(nerdamerRuntime);
} catch (evalError) {
console.warn(`Failed to evaluate test argument "${arg}": ${evalError}`);
return arg;
}
}
return arg;
});
}
// Helper function to extract return type from TypeScript function signature
function extractReturnType(functionDeclaration: tsMorph.FunctionDeclaration): string {
const returnTypeNode = functionDeclaration.getReturnTypeNode();
if (!returnTypeNode) {
return 'void';
}
const typeText = returnTypeNode.getText();
return typeText;
}
// Helper function to get function parameters
function getFunctionParameters(
functionDeclaration: tsMorph.FunctionDeclaration
): { name: string; type: string; optional: boolean; rest: boolean }[] {
const params = functionDeclaration.getParameters();
return params.map((param: tsMorph.ParameterDeclaration) => ({
name: param.getName(),
type: param.getTypeNode()?.getText() || 'unknown',
optional: param.hasQuestionToken(),
rest: param.isRestParameter(),
}));
}
// Robust type validation using TypeScript's own type system via ts-morph
function validateReturnType(
actualValue: unknown,
declaredType: string,
functionName: string,
typeNode?: tsMorph.TypeNode
): boolean {
// Use ts-morph's TypeScript integration for robust type checking
if (typeNode) {
try {
// Get the actual TypeScript Type object
const type = typeNode.getType();
if (!type) {
console.log(` 🚨 TypeScript could not resolve type: "${declaredType}"`);
return false;
}
// Check if this is a union type with a systematic approach
if (type.isUnion()) {
// For union types, check if the actual value matches any of the union members
const unionTypes = type.getUnionTypes();
return unionTypes.some((unionType: tsMorph.Type) => {
const unionTypeText = unionType.getSymbol()?.getName() || unionType.getText();
return validateSingleType(actualValue, unionTypeText, functionName);
});
}
// For non-union types, validate directly
return validateSingleType(actualValue, declaredType, functionName);
} catch (error) {
console.log(` 🚨 TypeScript type validation failed for "${declaredType}": ${error}`);
// Fall back to basic validation
}
}
// Fallback validation without ts-morph type information
return validateSingleType(actualValue, declaredType, functionName);
}
// Helper function to validate a single (non-union) type
function validateSingleType(actualValue: unknown, declaredType: string, functionName: string): boolean {
// Enhanced NerdamerExpression validation that detects object wrappers
function isProperNerdamerExpression(v: unknown): boolean {
if (v === undefined || v === null) {
return false;
}
if (typeof v !== 'object') {
return false;
}
const obj = v as Record<string, unknown>;
// Must have the required methods from CoreExpressionBase
if (typeof obj.toString !== 'function' || typeof obj['text'] !== 'function') {
return false;
}
// Must have a symbol property (from NerdamerExpression interface)
if (!('symbol' in obj)) {
return false;
}
// Critical check: symbol property should not be undefined for valid expressions
// This detects the vecget issue where invalid indices return { symbol: undefined }
if (obj['symbol'] === undefined) {
console.log(` 🔍 Detected object wrapper with undefined symbol for ${functionName}`);
return false;
}
// The symbol should be a proper NerdamerSymbol object with expected properties
if (obj['symbol'] && typeof obj['symbol'] === 'object') {
const symbolObj = obj['symbol'] as Record<string, unknown>;
const hasSymbolProps =
'group' in symbolObj &&
'value' in symbolObj &&
'multiplier' in symbolObj &&
'power' in symbolObj;
if (!hasSymbolProps) {
console.log(` 🔍 NerdamerSymbol object missing required properties for ${functionName}`);
return false;
}
}
return true;
}
// For object literal types like "{ symbol: ... }", do structural validation
if (declaredType.startsWith('{') && declaredType.endsWith('}')) {
if (!actualValue || typeof actualValue !== 'object') {
return false;
}
const obj = actualValue as Record<string, unknown>;
// Extract the expected property from the object literal type
if (declaredType.includes('symbol:')) {
if (!('symbol' in obj)) {
return false;
}
// Check if undefined is allowed in the symbol type
if (declaredType.includes('NerdamerSymbol | undefined')) {
// Allow symbol to be either a proper NerdamerSymbol object or undefined
return (
obj['symbol'] === undefined || (obj['symbol'] !== null && typeof obj['symbol'] === 'object')
);
}
if (declaredType.includes('NerdamerSymbol')) {
// NerdamerSymbol is required, undefined is NOT allowed
if (obj['symbol'] === undefined) {
console.log(
` 🚨 NerdamerSymbol property is undefined but type requires NerdamerSymbol (no undefined allowed)`
);
return false;
}
return obj['symbol'] !== null && typeof obj['symbol'] === 'object';
}
}
return true; // Generic object literal validation passed
}
// Standard type validation
const typeValidators: Record<string, (value: unknown) => boolean> = {
undefined: v => v === undefined,
string: v => typeof v === 'string',
number: v => typeof v === 'number',
boolean: v => typeof v === 'boolean',
void: v => v === undefined,
any: _v => true,
unknown: _v => true,
'string[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'string'),
'number[]': v => Array.isArray(v) && v.every((item: unknown) => typeof item === 'number'),
NerdamerExpression: isProperNerdamerExpression,
'NerdamerCore.Vector': v => {
const o = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && typeof o?.['toString'] === 'function';
},
'NerdamerCore.Matrix': v => {
const o = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && typeof o?.['toString'] === 'function';
},
'NerdamerCore.Set': v => {
const o = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && typeof o?.['toString'] === 'function';
},
NerdamerEquation: v => {
const o = v as Record<string, unknown> | null;
return v !== null && typeof v === 'object' && Boolean(o?.['LHS']) && Boolean(o?.['RHS']);
},
'typeof nerdamer': v => typeof v === 'function',
};
// Check for exact type match
const validator = typeValidators[declaredType];
if (validator) {
return validator(actualValue);
}
// Check for complex types containing known patterns
if (declaredType.includes('NerdamerExpression')) {
const exprValidator = typeValidators['NerdamerExpression'];
return exprValidator ? exprValidator(actualValue) : false;
}
if (declaredType.includes('Vector')) {
const vecValidator = typeValidators['NerdamerCore.Vector'];
return vecValidator ? vecValidator(actualValue) : false;
}
if (declaredType.includes('Matrix')) {
const matValidator = typeValidators['NerdamerCore.Matrix'];
return matValidator ? matValidator(actualValue) : false;
}
if (declaredType.includes('nerdamer')) {
const nerdValidator = typeValidators['typeof nerdamer'];
return nerdValidator ? nerdValidator(actualValue) : false;
}
// For unknown types, be conservative but log it
console.log(` ❌ Cannot validate unknown type "${declaredType}"`);
return false;
}
try {
// Find nerdamerPrime namespace
const nerdamerPrimeNamespace = sourceFile
.getDescendantsOfKind(tsMorph.SyntaxKind.ModuleDeclaration)
.find((ns: tsMorph.ModuleDeclaration) => ns.getName() === 'nerdamerPrime');
if (!nerdamerPrimeNamespace) {
metadataValidationErrors.push('nerdamerPrime namespace not found in type definitions');
expect(metadataValidationErrors).toEqual([]);
return;
}
// Get all function declarations from the namespace
const functionDeclarations = nerdamerPrimeNamespace.getDescendantsOfKind(
tsMorph.SyntaxKind.FunctionDeclaration
);
console.log(`\n=== Metadata-Enhanced Testing: Found ${functionDeclarations.length} functions ===`);
let functionsWithMetadata = 0;
let functionsWithoutMetadata = 0;
for (const funcDecl of functionDeclarations) {
const functionName = funcDecl.getName();
// Skip if function name is undefined
if (!functionName) {
console.log('Skipping function declaration without name');
continue;
}
const returnType = extractReturnType(funcDecl);
const parameters = getFunctionParameters(funcDecl);
const te