From c0144e869d52125e3d8ddbe9cf107ccce035df02 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Wed, 30 Oct 2024 11:31:14 +0200 Subject: [PATCH] --- Source/Artifact/variable-inliner (1).ts | 492 ++++++++++++++++++++++++ Source/Artifact/variable-inliner (2).ts | 286 ++++++++++++++ Source/Artifact/variable-inliner (3).ts | 137 +++++++ Source/Artifact/variable-inliner.ts | 354 +++++++++++++++++ 4 files changed, 1269 insertions(+) create mode 100644 Source/Artifact/variable-inliner (1).ts create mode 100644 Source/Artifact/variable-inliner (2).ts create mode 100644 Source/Artifact/variable-inliner (3).ts create mode 100644 Source/Artifact/variable-inliner.ts diff --git a/Source/Artifact/variable-inliner (1).ts b/Source/Artifact/variable-inliner (1).ts new file mode 100644 index 00000000..d117e2a5 --- /dev/null +++ b/Source/Artifact/variable-inliner (1).ts @@ -0,0 +1,492 @@ +import ts from "typescript"; + +interface InlinerOptions { + allowComplexExpressions?: boolean; + maxInlineDepth?: number; + inlineInFunctions?: boolean; + excludeVariables?: string[]; + includeVariables?: string[]; + // New options + preserveComments?: boolean; + generateSourceMaps?: boolean; + inlineDestructuring?: boolean; + optimizationLevel?: "conservative" | "aggressive"; + maxExpressionSize?: number; // Limit size of inlined expressions +} + +interface TransformationResult { + code: string; + sourceMap?: string; + statistics: TransformationStatistics; +} + +interface TransformationStatistics { + totalVariables: number; + inlinedVariables: number; + skippedVariables: { + scope: number; + complexity: number; + size: number; + excluded: number; + multiple: number; + }; + optimizationTime: number; +} + +// Expression complexity analyzer +class ExpressionAnalyzer { + private readonly maxSize: number; + + constructor(maxSize: number) { + this.maxSize = maxSize; + } + + getExpressionSize(node: ts.Expression): number { + let size = 0; + const visit = (node: ts.Node) => { + size++; + ts.forEachChild(node, visit); + }; + visit(node); + return size; + } + + analyzeSideEffects(node: ts.Expression): boolean { + const hasSideEffects = (node: ts.Node): boolean => { + if (ts.isCallExpression(node) || ts.isNewExpression(node)) { + return true; + } + + if (ts.isPropertyAccessExpression(node)) { + // Getter might have side effects + return true; + } + + let result = false; + ts.forEachChild(node, (child) => { + if (hasSideEffects(child)) { + result = true; + } + }); + + return result; + }; + + return hasSideEffects(node); + } + + analyzeComplexity(node: ts.Expression): { + safe: boolean; + reason?: string; + } { + if (this.getExpressionSize(node) > this.maxSize) { + return { safe: false, reason: "Expression too large" }; + } + + if (this.analyzeSideEffects(node)) { + return { safe: false, reason: "Contains side effects" }; + } + + // Check for potential runtime errors + const hasRuntimeRisks = (node: ts.Node): boolean => { + if ( + ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) + ) { + // Could throw if accessing undefined + return true; + } + + if (ts.isBinaryExpression(node)) { + const op = node.operatorToken.kind; + // Division could throw + if (op === ts.SyntaxKind.SlashToken) { + return true; + } + } + + let result = false; + ts.forEachChild(node, (child) => { + if (hasRuntimeRisks(child)) { + result = true; + } + }); + + return result; + }; + + if (hasRuntimeRisks(node)) { + return { safe: false, reason: "Potential runtime errors" }; + } + + return { safe: true }; + } +} + +// Scope analyzer for handling destructuring and complex patterns +class ScopeAnalyzer { + private readonly typeChecker: ts.TypeChecker; + private readonly scopeMap = new Map>(); + + constructor(typeChecker: ts.TypeChecker) { + this.typeChecker = typeChecker; + } + + analyzeScope(node: ts.Node) { + if ( + ts.isSourceFile(node) || + ts.isBlock(node) || + ts.isFunctionLike(node) + ) { + const scope = new Set(); + this.scopeMap.set(node, scope); + + // Collect declarations in this scope + const visitNode = (child: ts.Node) => { + if (ts.isVariableDeclaration(child)) { + this.collectBindings(child.name, scope); + } + ts.forEachChild(child, visitNode); + }; + + ts.forEachChild(node, visitNode); + } + } + + private collectBindings(name: ts.BindingName, scope: Set) { + if (ts.isIdentifier(name)) { + const symbol = this.typeChecker.getSymbolAtLocation(name); + if (symbol) { + scope.add(symbol); + } + } else if ( + ts.isObjectBindingPattern(name) || + ts.isArrayBindingPattern(name) + ) { + for (const element of name.elements) { + if (ts.isBindingElement(element)) { + this.collectBindings(element.name, scope); + } + } + } + } + + isInScope(symbol: ts.Symbol, node: ts.Node): boolean { + let current: ts.Node | undefined = node; + while (current) { + const scope = this.scopeMap.get(current); + if (scope?.has(symbol)) { + return true; + } + current = current.parent; + } + return false; + } +} + +class VariableInliner { + private readonly options: Required; + private readonly expressionAnalyzer: ExpressionAnalyzer; + private readonly statistics: TransformationStatistics; + private scopeAnalyzer!: ScopeAnalyzer; + + constructor(options: InlinerOptions = {}) { + this.options = { + allowComplexExpressions: false, + maxInlineDepth: 3, + inlineInFunctions: true, + excludeVariables: [], + includeVariables: [], + preserveComments: true, + generateSourceMaps: false, + inlineDestructuring: true, + optimizationLevel: "conservative", + maxExpressionSize: 100, + ...options, + }; + + this.expressionAnalyzer = new ExpressionAnalyzer( + this.options.maxExpressionSize, + ); + this.statistics = { + totalVariables: 0, + inlinedVariables: 0, + skippedVariables: { + scope: 0, + complexity: 0, + size: 0, + excluded: 0, + multiple: 0, + }, + optimizationTime: 0, + }; + } + + private handleDestructuring( + pattern: ts.BindingPattern, + initializer: ts.Expression, + ): ts.Expression { + if (ts.isObjectBindingPattern(pattern)) { + const properties: ts.PropertyAssignment[] = []; + + for (const element of pattern.elements) { + if (ts.isBindingElement(element)) { + const propName = element.propertyName || element.name; + if (ts.isIdentifier(propName)) { + properties.push( + ts.factory.createPropertyAssignment( + propName.text, + element.initializer || + ts.factory.createIdentifier(propName.text), + ), + ); + } + } + } + + return ts.factory.createObjectLiteralExpression(properties); + } + + if (ts.isArrayBindingPattern(pattern)) { + const elements: ts.Expression[] = pattern.elements.map( + (element) => { + if (ts.isBindingElement(element)) { + return ( + element.initializer || + ts.factory.createIdentifier("undefined") + ); + } + return ts.factory.createIdentifier("undefined"); + }, + ); + + return ts.factory.createArrayLiteralExpression(elements); + } + + throw new Error("Unsupported binding pattern type"); + } + + private shouldInlineExpression(node: ts.Expression): boolean { + const analysis = this.expressionAnalyzer.analyzeComplexity(node); + if (!analysis.safe) { + this.statistics.skippedVariables.complexity++; + return false; + } + + const size = this.expressionAnalyzer.getExpressionSize(node); + if (size > this.options.maxExpressionSize) { + this.statistics.skippedVariables.size++; + return false; + } + + return true; + } + + public transform( + sourceFile: ts.SourceFile, + program: ts.Program, + ): TransformationResult { + const startTime = Date.now(); + const typeChecker = program.getTypeChecker(); + this.scopeAnalyzer = new ScopeAnalyzer(typeChecker); + + // First pass: analyze scopes + const analyzeNode = (node: ts.Node) => { + this.scopeAnalyzer.analyzeScope(node); + ts.forEachChild(node, analyzeNode); + }; + analyzeNode(sourceFile); + + // Second pass: perform transformation + const transformer = (context: ts.TransformationContext) => { + const visit = (node: ts.Node): ts.Node => { + if (ts.isVariableStatement(node)) { + this.statistics.totalVariables += + node.declarationList.declarations.length; + + return this.transformVariableStatement( + node, + typeChecker, + context, + ); + } + + return ts.visitEachChild(node, visit, context); + }; + return visit; + }; + + const transformed = ts.transform(sourceFile, [transformer]) + .transformed[0]; + + // Generate output + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + removeComments: !this.options.preserveComments, + }); + + this.statistics.optimizationTime = Date.now() - startTime; + + const result: TransformationResult = { + code: printer.printNode( + ts.EmitHint.SourceFile, + transformed, + sourceFile, + ), + statistics: this.statistics, + }; + + if (this.options.generateSourceMaps) { + // Source map generation would go here + result.sourceMap = ""; // TODO: Implement source map generation + } + + return result; + } + + private transformVariableStatement( + node: ts.VariableStatement, + typeChecker: ts.TypeChecker, + context: ts.TransformationContext, + ): ts.Node { + const declarations = node.declarationList.declarations.filter( + (decl) => { + const name = decl.name; + + if (ts.isIdentifier(name)) { + if (this.isVariableExcluded(name.text)) { + this.statistics.skippedVariables.excluded++; + return true; + } + + const symbol = typeChecker.getSymbolAtLocation(name); + if (!symbol) return true; + + // Check usage count + const references = this.findReferences(symbol, typeChecker); + if (references.length !== 1) { + this.statistics.skippedVariables.multiple++; + return true; + } + + // Check if we can inline the initializer + if ( + decl.initializer && + this.shouldInlineExpression(decl.initializer) + ) { + this.statistics.inlinedVariables++; + return false; + } + } else if ( + ts.isBindingPattern(name) && + this.options.inlineDestructuring + ) { + // Handle destructuring patterns + return !this.canInlineDestructuring(name, decl.initializer); + } + + return true; + }, + ); + + if (declarations.length === 0) { + return ts.factory.createEmptyStatement(); + } + + return ts.factory.updateVariableStatement( + node, + node.modifiers, + ts.factory.createVariableDeclarationList( + declarations, + node.declarationList.flags, + ), + ); + } + + private canInlineDestructuring( + pattern: ts.BindingPattern, + initializer: ts.Expression | undefined, + ): boolean { + if (!initializer) return false; + + // Check if the initializer is safe to inline + if (!this.shouldInlineExpression(initializer)) { + return false; + } + + // For object patterns, ensure all properties exist and are safe + if (ts.isObjectBindingPattern(pattern)) { + for (const element of pattern.elements) { + if (!ts.isBindingElement(element)) continue; + + if ( + element.initializer && + !this.shouldInlineExpression(element.initializer) + ) { + return false; + } + } + } + + return true; + } + + private findReferences( + symbol: ts.Symbol, + typeChecker: ts.TypeChecker, + ): ts.Identifier[] { + const references: ts.Identifier[] = []; + const root = symbol.declarations?.[0]?.getSourceFile(); + + if (!root) return references; + + const visit = (node: ts.Node) => { + if (ts.isIdentifier(node)) { + const nodeSymbol = typeChecker.getSymbolAtLocation(node); + if (nodeSymbol === symbol) { + references.push(node); + } + } + ts.forEachChild(node, visit); + }; + + visit(root); + return references; + } + + private isVariableExcluded(name: string): boolean { + if (this.options.includeVariables.length > 0) { + return !this.options.includeVariables.includes(name); + } + return this.options.excludeVariables.includes(name); + } +} + +// Example usage with the new features +async function optimizeTypeScriptFile( + filePath: string, + options: InlinerOptions = {}, +): Promise { + const program = ts.createProgram([filePath], { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS, + }); + + const sourceFile = program.getSourceFile(filePath); + if (!sourceFile) { + throw new Error(`Source file '${filePath}' not found`); + } + + const inliner = new VariableInliner(options); + return inliner.transform(sourceFile, program); +} + +export { + VariableInliner, + optimizeTypeScriptFile, + InlinerOptions, + TransformationResult, + TransformationStatistics, + ExpressionAnalyzer, + ScopeAnalyzer, +}; diff --git a/Source/Artifact/variable-inliner (2).ts b/Source/Artifact/variable-inliner (2).ts new file mode 100644 index 00000000..eb987ac3 --- /dev/null +++ b/Source/Artifact/variable-inliner (2).ts @@ -0,0 +1,286 @@ +import ts from "typescript"; + +interface InlinerOptions { + // Allow inlining of complex expressions (function calls, new expressions, etc) + allowComplexExpressions?: boolean; + // Maximum depth for recursive inlining + maxInlineDepth?: number; + // Allow inlining inside functions + inlineInFunctions?: boolean; + // Skip variables with specific names + excludeVariables?: string[]; + // Only inline variables with specific names + includeVariables?: string[]; +} + +class VariableInliner { + private variableUsages = new Map< + ts.Symbol, + { + declaration: ts.VariableDeclaration; + references: ts.Identifier[]; + canInline: boolean; + scope: ts.Node; + inlineDepth: number; + } + >(); + + private currentScope: ts.Node | undefined; + private inlineDepth = 0; + private readonly options: Required; + + constructor(options: InlinerOptions = {}) { + this.options = { + allowComplexExpressions: false, + maxInlineDepth: 3, + inlineInFunctions: true, + excludeVariables: [], + includeVariables: [], + ...options, + }; + } + + private isVariableExcluded(name: string): boolean { + if (this.options.includeVariables.length > 0) { + return !this.options.includeVariables.includes(name); + } + return this.options.excludeVariables.includes(name); + } + + private isInlinable(node: ts.Expression, depth: number = 0): boolean { + if (depth > this.options.maxInlineDepth) { + return false; + } + + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.UndefinedKeyword: + return true; + + case ts.SyntaxKind.Identifier: + return true; + + case ts.SyntaxKind.ParenthesizedExpression: + return this.isInlinable( + (node as ts.ParenthesizedExpression).expression, + depth + 1, + ); + + case ts.SyntaxKind.BinaryExpression: { + const binExp = node as ts.BinaryExpression; + return ( + this.isInlinable(binExp.left, depth + 1) && + this.isInlinable(binExp.right, depth + 1) + ); + } + + case ts.SyntaxKind.PropertyAccessExpression: { + const propAccess = node as ts.PropertyAccessExpression; + return this.isInlinable(propAccess.expression, depth + 1); + } + + case ts.SyntaxKind.ElementAccessExpression: { + const elemAccess = node as ts.ElementAccessExpression; + return ( + this.isInlinable(elemAccess.expression, depth + 1) && + this.isInlinable(elemAccess.argumentExpression, depth + 1) + ); + } + + case ts.SyntaxKind.ConditionalExpression: { + const condExp = node as ts.ConditionalExpression; + return ( + this.isInlinable(condExp.condition, depth + 1) && + this.isInlinable(condExp.whenTrue, depth + 1) && + this.isInlinable(condExp.whenFalse, depth + 1) + ); + } + + case ts.SyntaxKind.CallExpression: + case ts.SyntaxKind.NewExpression: + return this.options.allowComplexExpressions; + + default: + return false; + } + } + + private shouldInlineInCurrentScope( + declaration: ts.VariableDeclaration, + reference: ts.Identifier, + ): boolean { + if (!this.currentScope || !declaration.parent) { + return false; + } + + // Check if the reference is within the same scope or a nested scope + let referenceScope: ts.Node | undefined = reference; + while (referenceScope && referenceScope !== this.currentScope) { + if ( + !this.options.inlineInFunctions && + ts.isFunctionLike(referenceScope) + ) { + return false; + } + referenceScope = referenceScope.parent; + } + + return referenceScope === this.currentScope; + } + + private analyzeNode(node: ts.Node, typeChecker: ts.TypeChecker): void { + const previousScope = this.currentScope; + + if (ts.isSourceFile(node) || ts.isBlock(node)) { + this.currentScope = node; + } + + if (ts.isVariableDeclaration(node)) { + const symbol = typeChecker.getSymbolAtLocation(node.name); + if ( + symbol && + node.initializer && + !this.isVariableExcluded(node.name.getText()) + ) { + this.variableUsages.set(symbol, { + declaration: node, + references: [], + canInline: this.isInlinable(node.initializer), + scope: this.currentScope!, + inlineDepth: 0, + }); + } + } + + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol && this.variableUsages.has(symbol)) { + const usage = this.variableUsages.get(symbol)!; + if (this.shouldInlineInCurrentScope(usage.declaration, node)) { + usage.references.push(node); + } + } + } + + ts.forEachChild(node, (child) => this.analyzeNode(child, typeChecker)); + this.currentScope = previousScope; + } + + private createTransformer( + typeChecker: ts.TypeChecker, + ): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + const visit = (node: ts.Node): ts.Node => { + if (ts.isVariableStatement(node)) { + const declarations = + node.declarationList.declarations.filter((decl) => { + const symbol = typeChecker.getSymbolAtLocation( + decl.name, + ); + if (!symbol) return true; + + const usage = this.variableUsages.get(symbol); + return ( + !usage || + usage.references.length !== 1 || + !usage.canInline || + usage.inlineDepth >= this.options.maxInlineDepth + ); + }); + + if (declarations.length === 0) { + return ts.factory.createEmptyStatement(); + } + + if ( + declarations.length !== + node.declarationList.declarations.length + ) { + return ts.factory.updateVariableStatement( + node, + node.modifiers, + ts.factory.createVariableDeclarationList( + declarations, + node.declarationList.flags, + ), + ); + } + } + + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + const usage = this.variableUsages.get(symbol); + if ( + usage && + usage.references.length === 1 && + usage.canInline && + usage.inlineDepth < this.options.maxInlineDepth + ) { + usage.inlineDepth++; + return usage.declaration.initializer!; + } + } + } + + return ts.visitEachChild(node, visit, context); + }; + + return visit; + }; + } + + public transform(sourceFile: ts.SourceFile, program: ts.Program): ts.Node { + const typeChecker = program.getTypeChecker(); + + // Reset state + this.variableUsages.clear(); + this.currentScope = undefined; + this.inlineDepth = 0; + + // Analyze the source file + this.analyzeNode(sourceFile, typeChecker); + + // Transform the source file + return ts.transform(sourceFile, [this.createTransformer(typeChecker)]) + .transformed[0]; + } +} + +// Helper function to transform a file +function transformFile(sourceFile: string, options?: InlinerOptions) { + const program = ts.createProgram([sourceFile], { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS, + }); + + const sourceFileObj = program.getSourceFile(sourceFile); + if (!sourceFileObj) { + throw new Error(`Source file '${sourceFile}' not found`); + } + + const inliner = new VariableInliner(options); + const result = inliner.transform(sourceFileObj, program); + + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + removeComments: false, + }); + + return printer.printNode(ts.EmitHint.SourceFile, result, sourceFileObj); +} + +// Example usage with various configurations +const exampleOptions: InlinerOptions = { + allowComplexExpressions: true, + maxInlineDepth: 2, + inlineInFunctions: true, + excludeVariables: ["debug", "logger"], + includeVariables: [], +}; + +export { VariableInliner, transformFile, InlinerOptions }; diff --git a/Source/Artifact/variable-inliner (3).ts b/Source/Artifact/variable-inliner (3).ts new file mode 100644 index 00000000..e0f2672c --- /dev/null +++ b/Source/Artifact/variable-inliner (3).ts @@ -0,0 +1,137 @@ +import ts from 'typescript'; + +function createVariableInliner() { + // Track variable declarations and their usage count + const variableUsages = new Map(); + + function visitNode(node: ts.Node, program: ts.Program, typeChecker: ts.TypeChecker): ts.Node { + // Track variable declarations + if (ts.isVariableDeclaration(node)) { + const symbol = typeChecker.getSymbolAtLocation(node.name); + if (symbol && node.initializer) { + variableUsages.set(symbol, { + declaration: node, + references: [], + canInline: isInlinable(node.initializer) + }); + } + } + + // Track variable references + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol && variableUsages.has(symbol)) { + const usage = variableUsages.get(symbol)!; + usage.references.push(node); + } + } + + return ts.visitEachChild( + node, + child => visitNode(child, program, typeChecker), + context + ); + } + + function isInlinable(node: ts.Expression): boolean { + // Determine if an expression can be safely inlined + switch (node.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NumericLiteral: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.NullKeyword: + case ts.SyntaxKind.Identifier: + return true; + case ts.SyntaxKind.ParenthesizedExpression: + return isInlinable((node as ts.ParenthesizedExpression).expression); + case ts.SyntaxKind.BinaryExpression: { + const binExp = node as ts.BinaryExpression; + return isInlinable(binExp.left) && isInlinable(binExp.right); + } + default: + return false; + } + } + + function inlineVariables(node: ts.Node, program: ts.Program, typeChecker: ts.TypeChecker): ts.Node { + // First pass: collect usage information + visitNode(node, program, typeChecker); + + // Second pass: perform inlining + const transformer = (context: ts.TransformationContext) => { + const visit = (node: ts.Node): ts.Node => { + // Handle variable declarations + if (ts.isVariableStatement(node)) { + const declarations = node.declarationList.declarations.filter(decl => { + const symbol = typeChecker.getSymbolAtLocation(decl.name); + if (!symbol) return true; + + const usage = variableUsages.get(symbol); + return !usage || usage.references.length !== 1 || !usage.canInline; + }); + + if (declarations.length === 0) { + return ts.createEmptyStatement(); + } + + if (declarations.length !== node.declarationList.declarations.length) { + return ts.factory.updateVariableStatement( + node, + node.modifiers, + ts.factory.createVariableDeclarationList(declarations, node.declarationList.flags) + ); + } + } + + // Handle variable references + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + const usage = variableUsages.get(symbol); + if (usage && usage.references.length === 1 && usage.canInline) { + return usage.declaration.initializer!; + } + } + } + + return ts.visitEachChild(node, visit, context); + }; + + return visit; + }; + + return ts.transform(node, [transformer]).transformed[0]; + } + + return { + inlineVariables + }; +} + +// Example usage +function transformFile(sourceFile: string) { + const program = ts.createProgram([sourceFile], { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS + }); + + const checker = program.getTypeChecker(); + const sourceFile = program.getSourceFile(sourceFile); + + if (!sourceFile) { + throw new Error('Source file not found'); + } + + const inliner = createVariableInliner(); + const result = inliner.inlineVariables(sourceFile, program, checker); + + const printer = ts.createPrinter(); + return printer.printNode(ts.EmitHint.SourceFile, result, sourceFile); +} + +export { createVariableInliner, transformFile }; diff --git a/Source/Artifact/variable-inliner.ts b/Source/Artifact/variable-inliner.ts new file mode 100644 index 00000000..a7b9c529 --- /dev/null +++ b/Source/Artifact/variable-inliner.ts @@ -0,0 +1,354 @@ +// test-utils.ts +import { expect } from "chai"; +import ts from "typescript"; + +import { + InlinerOptions, + TransformationResult, + VariableInliner, +} from "./inliner"; + +interface TestCase { + name: string; + input: string; + expected: string; + options?: InlinerOptions; +} + +class TestRunner { + private compiler: ts.CompilerHost; + private fileMap: Map = new Map(); + + constructor() { + this.compiler = this.createCompilerHost(); + } + + private createCompilerHost(): ts.CompilerHost { + return { + getSourceFile: (fileName: string, languageVersion) => { + const sourceText = this.fileMap.get(fileName); + return sourceText + ? ts.createSourceFile(fileName, sourceText, languageVersion) + : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: () => {}, + getCurrentDirectory: () => "", + getCanonicalFileName: (fileName) => fileName, + useCaseSensitiveFileNames: () => true, + getNewLine: () => "\n", + fileExists: (fileName) => this.fileMap.has(fileName), + readFile: (fileName) => this.fileMap.get(fileName), + }; + } + + async runTest(testCase: TestCase): Promise { + const fileName = `test-${testCase.name}.ts`; + this.fileMap.set(fileName, testCase.input); + + const program = ts.createProgram( + [fileName], + { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.CommonJS, + }, + this.compiler, + ); + + const inliner = new VariableInliner(testCase.options); + const sourceFile = program.getSourceFile(fileName); + if (!sourceFile) { + throw new Error( + `Failed to create source file for test ${testCase.name}`, + ); + } + + const result = inliner.transform(sourceFile, program); + + // Normalize whitespace for comparison + const normalizedResult = result.code.replace(/\s+/g, " ").trim(); + const normalizedExpected = testCase.expected + .replace(/\s+/g, " ") + .trim(); + + expect(normalizedResult).to.equal(normalizedExpected); + } +} + +// validation.ts +class TypeScriptValidator { + validate(node: ts.Node, typeChecker: ts.TypeChecker): ValidationResult { + const errors: ValidationError[] = []; + + const visit = (node: ts.Node) => { + // Validate type compatibility + if (ts.isVariableDeclaration(node) && node.initializer) { + const declType = typeChecker.getTypeAtLocation(node.name); + const initType = typeChecker.getTypeAtLocation( + node.initializer, + ); + + if (!typeChecker.isTypeAssignableTo(initType, declType)) { + errors.push({ + node, + message: "Type mismatch in variable declaration", + category: "type", + }); + } + } + + // Validate reference integrity + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol && symbol.declarations) { + const declaration = symbol.declarations[0]; + if (ts.isVariableDeclaration(declaration)) { + const scope = this.findEnclosingScope(node); + const declScope = this.findEnclosingScope(declaration); + + if (!this.isAccessibleFrom(scope, declScope)) { + errors.push({ + node, + message: + "Variable reference violates scope rules", + category: "scope", + }); + } + } + } + } + + ts.forEachChild(node, visit); + }; + + visit(node); + return new ValidationResult(errors); + } + + private findEnclosingScope(node: ts.Node): ts.Node { + let current = node; + while (current) { + if ( + ts.isSourceFile(current) || + ts.isBlock(current) || + ts.isFunctionLike(current) + ) { + return current; + } + current = current.parent; + } + return node.getSourceFile(); + } + + private isAccessibleFrom( + currentScope: ts.Node, + targetScope: ts.Node, + ): boolean { + let scope = currentScope; + while (scope) { + if (scope === targetScope) return true; + scope = scope.parent; + } + return false; + } +} + +class ValidationResult { + constructor(private errors: ValidationError[]) {} + + hasErrors(): boolean { + return this.errors.length > 0; + } + + getErrors(): ValidationError[] { + return [...this.errors]; + } + + toString(): string { + return this.errors + .map((error) => `${error.category.toUpperCase()}: ${error.message}`) + .join("\n"); + } +} + +interface ValidationError { + node: ts.Node; + message: string; + category: "type" | "scope" | "syntax"; +} + +// Advanced TypeScript features support +class TypeScriptFeatureHandler { + private typeChecker: ts.TypeChecker; + + constructor(typeChecker: ts.TypeChecker) { + this.typeChecker = typeChecker; + } + + handleDecorators(node: ts.Node): ts.Node { + if (!ts.canHaveDecorators(node)) return node; + + const decorators = ts.getDecorators(node); + if (!decorators) return node; + + // Analyze decorator impact + for (const decorator of decorators) { + const symbol = this.typeChecker.getSymbolAtLocation( + decorator.expression, + ); + if (!symbol) continue; + + // Check if decorator affects inlining + if (this.isInliningAffectingDecorator(symbol)) { + return node; // Skip inlining for decorated nodes + } + } + + return node; + } + + private isInliningAffectingDecorator(symbol: ts.Symbol): boolean { + // Check for known decorators that affect variable behavior + const name = symbol.getName(); + return ["observable", "computed", "action"].includes(name); + } + + handleNamespaces(node: ts.Node): ts.Node { + if (ts.isModuleDeclaration(node)) { + // Handle namespace-specific transformations + const transformer = (context: ts.TransformationContext) => { + const visit = (node: ts.Node): ts.Node => { + if (ts.isVariableStatement(node)) { + // Special handling for namespace variables + return this.transformNamespaceVariable(node); + } + return ts.visitEachChild(node, visit, context); + }; + return visit; + }; + + return ts.transform(node, [transformer]).transformed[0]; + } + return node; + } + + private transformNamespaceVariable(node: ts.VariableStatement): ts.Node { + // Special handling for namespace variables + const declarations = node.declarationList.declarations.map((decl) => { + if (ts.isIdentifier(decl.name)) { + const symbol = this.typeChecker.getSymbolAtLocation(decl.name); + if (symbol && this.isExported(symbol)) { + // Don't inline exported namespace variables + return decl; + } + } + return decl; + }); + + return ts.factory.updateVariableStatement( + node, + node.modifiers, + ts.factory.createVariableDeclarationList( + declarations, + node.declarationList.flags, + ), + ); + } + + private isExported(symbol: ts.Symbol): boolean { + return !!(symbol.flags & ts.SymbolFlags.Exported); + } +} + +// Test cases +const testCases: TestCase[] = [ + { + name: "basic-inlining", + input: ` + const x = 5; + const y = x + 3; + console.log(y); + `, + expected: ` + console.log(5 + 3); + `, + }, + { + name: "destructuring", + input: ` + const obj = { a: 1, b: 2 }; + const { a, b } = obj; + console.log(a + b); + `, + expected: ` + const obj = { a: 1, b: 2 }; + console.log(obj.a + obj.b); + `, + options: { inlineDestructuring: true }, + }, + { + name: "decorator-preservation", + input: ` + class Example { + @observable + x = 5; + + @computed + get doubled() { + return this.x * 2; + } + } + `, + expected: ` + class Example { + @observable + x = 5; + + @computed + get doubled() { + return this.x * 2; + } + } + `, + }, + { + name: "namespace-handling", + input: ` + namespace MyNamespace { + export const x = 5; + const y = x + 3; + export const z = y * 2; + } + `, + expected: ` + namespace MyNamespace { + export const x = 5; + export const z = (x + 3) * 2; + } + `, + }, +]; + +// Run tests +async function runAllTests() { + const runner = new TestRunner(); + + for (const testCase of testCases) { + try { + await runner.runTest(testCase); + console.log(`✓ Test passed: ${testCase.name}`); + } catch (error) { + console.error(`✗ Test failed: ${testCase.name}`); + console.error(error); + } + } +} + +export { + TestRunner, + TypeScriptValidator, + ValidationResult, + TypeScriptFeatureHandler, + testCases, + runAllTests, +};