nerdamer-prime
Version:
javascript light-weight symbolic math library
469 lines (385 loc) • 21.4 kB
text/typescript
// Load complete nerdamer with all modules
import nerdamerRuntime from '../all';
import tsMorph from 'ts-morph';
// Type alias for dynamic property access in reflection tests
type Indexable = Record<string, any>;
const nerdamer = nerdamerRuntime as typeof nerdamerRuntime & Indexable;
describe('Nerdamer API Surface Reflection Test', () => {
let tsApiExports: string[];
let tsExportedDeclarations: ReadonlyMap<string, tsMorph.ExportedDeclarations[]>;
let project: tsMorph.Project;
let sourceFile: tsMorph.SourceFile;
let nerdamerNamespace: tsMorph.ModuleDeclaration;
const jsApiExports = Object.keys(nerdamerRuntime).sort();
beforeAll(() => {
project = new tsMorph.Project({
tsConfigFilePath: 'tsconfig.json',
});
sourceFile = project.getSourceFileOrThrow('index.d.ts');
nerdamerNamespace = sourceFile.getModuleOrThrow('nerdamer');
// Get all exported declarations for detailed analysis
tsExportedDeclarations = nerdamerNamespace.getExportedDeclarations();
// Get all exported functions, variables, etc. from the namespace
tsApiExports = Array.from(tsExportedDeclarations.keys())
.filter(
(name: string) =>
// Filter out internal types and interfaces that aren't expected on the runtime object
// NerdamerSet is an exception - it's a renamed Set to avoid conflicts with native Set
(!name.startsWith('Nerdamer') || name === 'NerdamerSet') &&
!name.endsWith('Constructor') &&
!name.includes('Interface') &&
name !== 'NerdamerCore' &&
// Filter out type-only exports (type aliases) that have no runtime representation
![
'OutputType',
'ParseOption',
'ExpressionParam',
'ExpandOptions',
'ArithmeticOperand',
'LaTeXToken',
'FilteredLaTeXToken',
'SortFn',
'SolveResult',
].includes(name)
)
.sort();
});
it('should have a type definition for every runtime property', () => {
const missingInTs = jsApiExports.filter(p => !tsApiExports.includes(p));
if (missingInTs.length > 0) {
console.log('Missing in TypeScript definitions:', missingInTs);
}
expect(missingInTs).toEqual([]);
});
it('should have a runtime implementation for every type definition', () => {
const missingInJs = tsApiExports.filter(p => !jsApiExports.includes(p));
if (missingInJs.length > 0) {
console.log('Missing in JavaScript runtime:', missingInJs);
}
expect(missingInJs).toEqual([]);
});
it('should have correct function vs property distinction', () => {
const typeErrors: string[] = [];
const importAliasInfo: string[] = [];
for (const exportName of tsApiExports) {
const runtimeValue = nerdamer[exportName];
const tsDeclarations = tsExportedDeclarations.get(exportName) || [];
if (tsDeclarations.length === 0) {
continue;
}
const tsDeclaration = tsDeclarations[0]!;
const isRuntimeFunction = typeof runtimeValue === 'function';
// Check if TS declaration indicates it's a function
let isTsFunction = false;
// Use type guard for declarations that have getKind
const declarationWithKind = tsDeclaration as { getKind?: () => number; getKindName?: () => string };
if (declarationWithKind.getKind) {
const kind = declarationWithKind.getKind();
const kindName = declarationWithKind.getKindName?.() || 'Unknown';
// Handle direct function declarations
if (kind === tsMorph.SyntaxKind.FunctionDeclaration || kind === tsMorph.SyntaxKind.MethodSignature) {
isTsFunction = true;
}
// Handle function type property signatures
else if (kind === tsMorph.SyntaxKind.PropertySignature) {
// Cast to PropertySignature to access getTypeNode (must go through unknown first)
const propSig = tsDeclaration as unknown as tsMorph.PropertySignature;
if (propSig.getTypeNode?.()?.getKind() === tsMorph.SyntaxKind.FunctionType) {
isTsFunction = true;
}
}
// Handle export import assignments (like: export import cos = nerdamerPrime.cos)
// These are the most common pattern in this codebase
else if (
kind === tsMorph.SyntaxKind.ImportEqualsDeclaration ||
kind === tsMorph.SyntaxKind.ImportClause ||
kind === tsMorph.SyntaxKind.ImportDeclaration ||
kind === tsMorph.SyntaxKind.ExportDeclaration
) {
// For import/export declarations, we need to check what they're importing
// In this case, we assume they're importing functions from nerdamerPrime
// which means they should be functions if the runtime is a function
importAliasInfo.push(`${exportName}: TS import/export declaration (${kindName})`);
isTsFunction = isRuntimeFunction; // Assume correctness for import aliases
}
// Handle namespace imports (export import syntax creates these)
else if (kind === tsMorph.SyntaxKind.NamespaceImport) {
importAliasInfo.push(`${exportName}: TS namespace import`);
isTsFunction = isRuntimeFunction; // Assume correctness for namespace imports
}
}
// Only flag as error if we have confidence in our TS function detection
// and there's a clear mismatch
if (
isRuntimeFunction &&
!isTsFunction &&
declarationWithKind.getKind &&
declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportEqualsDeclaration &&
declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportClause &&
declarationWithKind.getKind() !== tsMorph.SyntaxKind.ImportDeclaration &&
declarationWithKind.getKind() !== tsMorph.SyntaxKind.ExportDeclaration &&
declarationWithKind.getKind() !== tsMorph.SyntaxKind.NamespaceImport
) {
typeErrors.push(
`${exportName}: Runtime is function but TS declares as non-function (kind: ${declarationWithKind.getKindName?.() || 'unknown'})`
);
} else if (
!isRuntimeFunction &&
isTsFunction &&
declarationWithKind.getKind &&
(declarationWithKind.getKind() === tsMorph.SyntaxKind.FunctionDeclaration ||
declarationWithKind.getKind() === tsMorph.SyntaxKind.MethodSignature)
) {
typeErrors.push(`${exportName}: TS declares as function but runtime is ${typeof runtimeValue}`);
}
}
if (importAliasInfo.length > 0) {
console.log('Import alias declarations found:', importAliasInfo.length);
console.log('Sample import aliases:', importAliasInfo.slice(0, 5));
}
if (typeErrors.length > 0) {
console.log('Function/Property type mismatches:', typeErrors);
}
// We expect very few type errors since most functions are import aliases
expect(typeErrors.length).toBeLessThan(5);
});
it('should have reasonable function arity for declared functions', () => {
const arityMismatches: string[] = [];
for (const exportName of tsApiExports) {
const runtimeValue = nerdamer[exportName];
const tsDeclarations = tsExportedDeclarations.get(exportName) || [];
if (typeof runtimeValue !== 'function' || tsDeclarations.length === 0) {
continue;
}
const tsDeclaration = tsDeclarations[0]!;
// Try to get parameters from TS declaration using type guard
let tsParameterCount = 0;
const declarationWithParams = tsDeclaration as { getParameters?: () => tsMorph.ParameterDeclaration[] };
if (declarationWithParams.getParameters) {
const params = declarationWithParams.getParameters();
// Count required parameters (non-optional, non-rest)
tsParameterCount = params.filter(
(p: tsMorph.ParameterDeclaration) => !p.hasQuestionToken() && !p.isRestParameter()
).length;
}
const runtimeArity = runtimeValue.length;
// Allow some flexibility for variadic functions and optional parameters
// Flag only when there's a significant mismatch
if (runtimeArity > 0 && tsParameterCount > 0 && Math.abs(runtimeArity - tsParameterCount) > 2) {
arityMismatches.push(
`${exportName}: Runtime arity ${runtimeArity} vs TS required params ${tsParameterCount}`
);
}
}
if (arityMismatches.length > 0) {
console.log('Significant arity mismatches:', arityMismatches);
}
// For now, just log mismatches rather than failing the test
// since some functions may have complex parameter handling
expect(arityMismatches.length).toBeLessThan(jsApiExports.length * 0.5); // Allow up to 50% mismatches
});
it('should detect potentially problematic runtime functions', () => {
const issues: string[] = [];
for (const exportName of jsApiExports) {
const runtimeValue = nerdamer[exportName];
if (typeof runtimeValue === 'function') {
// Check for functions that might throw immediately
try {
// Test if function accepts no arguments without throwing
const stringForm = runtimeValue.toString();
// Flag functions that seem to require arguments but have arity 0
if (runtimeValue.length === 0 && stringForm.includes('arguments[0]')) {
issues.push(`${exportName}: Function has arity 0 but accesses arguments[0]`);
}
// Flag functions with unusual patterns
if (stringForm.includes('throw') && stringForm.length < 100) {
issues.push(`${exportName}: Function appears to primarily throw errors`);
}
} catch (error) {
// Function threw during inspection
issues.push(`${exportName}: Function threw during reflection: ${error}`);
}
} else if (runtimeValue === undefined) {
issues.push(`${exportName}: Runtime value is undefined`);
} else if (runtimeValue === null) {
issues.push(`${exportName}: Runtime value is null`);
}
}
if (issues.length > 0) {
console.log('Potential runtime issues:', issues);
}
// Log issues but don't fail test - these are informational
expect(issues.length).toBeLessThan(jsApiExports.length); // Just ensure not everything is broken
});
it('should categorize API exports by type', () => {
const categories = {
functions: [] as string[],
objects: [] as string[],
primitives: [] as string[],
undefined: [] as string[],
null: [] as string[],
};
for (const exportName of jsApiExports) {
const runtimeValue = nerdamer[exportName];
const type = typeof runtimeValue;
if (type === 'function') {
categories.functions.push(exportName);
} else if (type === 'object') {
if (runtimeValue === null) {
categories.null.push(exportName);
} else {
categories.objects.push(exportName);
}
} else if (type === 'undefined') {
categories.undefined.push(exportName);
} else {
categories.primitives.push(exportName);
}
}
console.log('\n=== API Export Categories ===');
console.log('Functions:', categories.functions.length, categories.functions);
console.log('Objects:', categories.objects.length, categories.objects);
console.log('Primitives:', categories.primitives.length, categories.primitives);
console.log('Undefined:', categories.undefined.length, categories.undefined);
console.log('Null:', categories.null.length, categories.null);
// Ensure we have a reasonable distribution
expect(categories.functions.length).toBeGreaterThan(0);
expect(categories.undefined.length + categories.null.length).toBeLessThan(jsApiExports.length * 0.1);
});
it('should validate TypeScript declaration structure', () => {
const structureIssues: string[] = [];
for (const exportName of tsApiExports) {
const tsDeclarations = tsExportedDeclarations.get(exportName) || [];
if (tsDeclarations.length === 0) {
structureIssues.push(`${exportName}: No TS declaration found`);
continue;
}
if (tsDeclarations.length > 1) {
// Multiple declarations might indicate overloads, which is fine
const kinds = tsDeclarations.map((d: tsMorph.ExportedDeclarations) =>
'getKindName' in d && typeof d.getKindName === 'function' ? d.getKindName() : 'Unknown'
);
console.log(`${exportName}: Multiple declarations (${kinds.join(', ')})`);
}
const tsDeclaration = tsDeclarations[0]!;
// Check for basic structure issues - use type guard
const declarationWithKind = tsDeclaration as { getKind?: () => number };
if (!declarationWithKind.getKind) {
structureIssues.push(`${exportName}: TS declaration missing getKind method`);
}
}
if (structureIssues.length > 0) {
console.log('TS declaration structure issues:', structureIssues);
}
expect(structureIssues).toEqual([]);
});
it('should validate import alias targets in nerdamerPrime namespace', () => {
// This test checks that import aliases like "export import cos = nerdamerPrime.cos"
// actually reference functions in the nerdamerPrime namespace
const aliasValidationErrors: string[] = [];
const namespaceAnalysis: Record<string, number> = {};
// Get the nerdamerPrime namespace declarations
const nerdamerPrimeNamespace = sourceFile
.getDescendantsOfKind(tsMorph.SyntaxKind.ModuleDeclaration)
.find((ns: tsMorph.ModuleDeclaration) => ns.getName() === 'nerdamerPrime');
if (nerdamerPrimeNamespace) {
const primeExports = nerdamerPrimeNamespace.getExportedDeclarations();
for (const exportName of tsApiExports) {
const runtimeValue = nerdamer[exportName];
const isRuntimeFunction = typeof runtimeValue === 'function';
// Check if this export has a corresponding declaration in nerdamerPrime
const primeDeclarations = primeExports.get(exportName) || [];
if (primeDeclarations.length > 0) {
const primeDeclaration = primeDeclarations[0];
if (primeDeclaration?.getKind) {
const kindName = primeDeclaration.getKindName?.() || 'Unknown';
namespaceAnalysis[kindName] = (namespaceAnalysis[kindName] || 0) + 1;
if (isRuntimeFunction) {
const kind = primeDeclaration.getKind();
const isPrimeFunction = kind === tsMorph.SyntaxKind.FunctionDeclaration;
if (!isPrimeFunction) {
aliasValidationErrors.push(
`${exportName}: Runtime is function but nerdamerPrime declares as ${kindName}`
);
}
}
}
}
}
console.log('\n=== nerdamerPrime Namespace Analysis ===');
console.log('Declaration types in nerdamerPrime:', namespaceAnalysis);
if (aliasValidationErrors.length > 0) {
console.log('Import alias validation errors (first 10):', aliasValidationErrors.slice(0, 10));
}
// Allow some mismatches since nerdamerPrime might have different declaration patterns
expect(aliasValidationErrors.length).toBeLessThan(tsApiExports.length * 0.2);
} else {
console.log('Warning: nerdamerPrime namespace not found');
// If namespace not found, just pass the test
expect(true).toBe(true);
}
});
it('should detect TypeScript declaration patterns and issues', () => {
// This test analyzes common TypeScript patterns used in the declarations
const declarationPatterns: Record<string, number> = {};
const potentialIssues: string[] = [];
for (const exportName of tsApiExports) {
const tsDeclarations = tsExportedDeclarations.get(exportName) || [];
const runtimeValue = nerdamer[exportName];
const isRuntimeFunction = typeof runtimeValue === 'function';
if (tsDeclarations.length > 0) {
const tsDeclaration = tsDeclarations[0]!;
// Use type guard for declarations that may have getKind
const declarationWithMethods = tsDeclaration as {
getKind?: () => number;
getKindName?: () => string;
};
if (declarationWithMethods.getKind) {
const kindName = declarationWithMethods.getKindName?.() || 'Unknown';
declarationPatterns[kindName] = (declarationPatterns[kindName] || 0) + 1;
// Look for potential issues
if (isRuntimeFunction && kindName.includes('Variable')) {
potentialIssues.push(`${exportName}: Function runtime but declared as ${kindName}`);
}
// Check for missing JSDoc or type annotations
const declWithJsDocs = tsDeclaration as { getJsDocs?: () => unknown[] };
if (declWithJsDocs.getJsDocs?.().length === 0 && kindName === 'FunctionDeclaration') {
// Note: Not flagging this as an error since many functions may not have JSDoc
}
}
}
}
console.log('\n=== TypeScript Declaration Patterns ===');
console.log('Declaration types used:', declarationPatterns);
if (potentialIssues.length > 0) {
console.log('Potential declaration issues:', potentialIssues.slice(0, 5));
}
// Since all functions use ImportEqualsDeclaration, we expect exactly 1 type
expect(Object.keys(declarationPatterns).length).toBe(1);
expect(potentialIssues.length).toBeLessThan(tsApiExports.length * 0.1);
// Validate that ImportEqualsDeclaration is the only pattern
expect(declarationPatterns['ImportEqualsDeclaration']).toBe(182);
});
it('should log detailed API surface comparison', () => {
console.log('\n=== Enhanced API Surface Analysis ===');
console.log('JavaScript API exports:', jsApiExports.length, 'items');
console.log('TypeScript API exports:', tsApiExports.length, 'items');
// Show first few exports for quick verification
console.log('\nFirst 10 JavaScript exports:', jsApiExports.slice(0, 10));
console.log('First 10 TypeScript exports:', tsApiExports.slice(0, 10));
// Show any items that are only in one side
const onlyInJs = jsApiExports.filter(p => !tsApiExports.includes(p));
const onlyInTs = tsApiExports.filter(p => !jsApiExports.includes(p));
if (onlyInJs.length > 0) {
console.log('\nOnly in JavaScript:', onlyInJs);
}
if (onlyInTs.length > 0) {
console.log('Only in TypeScript:', onlyInTs);
}
// Show overlap
const overlap = jsApiExports.filter(p => tsApiExports.includes(p));
console.log(
`\nAPI Surface overlap: ${overlap.length}/${Math.max(jsApiExports.length, tsApiExports.length)} (${Math.round((overlap.length / Math.max(jsApiExports.length, tsApiExports.length)) * 100)}%)`
);
});
});