diff --git a/package.json b/package.json index 23139ef..b171504 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "lint-staged": { "*": "prettier --ignore-unknown --write" }, + "dependencies": { + "cached-factory": "^0.0.2" + }, "devDependencies": { "@prettier/sync": "^0.5.0", "@release-it/conventional-changelog": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80beb98..b11eb4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + cached-factory: + specifier: ^0.0.2 + version: 0.0.2 devDependencies: '@prettier/sync': specifier: ^0.5.0 @@ -1356,6 +1360,10 @@ packages: resolution: {integrity: sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==} engines: {node: '>=14.16'} + cached-factory@0.0.2: + resolution: {integrity: sha512-4mhebGQ8YyvlDAX+zOHso5MezPi2pP11ZFE7vYhDIOJifsxEi7R5geB/nrLBYzQH5nwZ9kNsLAjM+WBj6osLDg==} + engines: {node: '>=18'} + call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -6148,6 +6156,8 @@ snapshots: normalize-url: 8.0.0 responselike: 3.0.0 + cached-factory@0.0.2: {} + call-bind@1.0.2: dependencies: function-bind: 1.1.1 diff --git a/src/getFunctionDeclarationFromCall.ts b/src/getFunctionDeclarationFromCall.ts index 4f393cb..32eb964 100644 --- a/src/getFunctionDeclarationFromCall.ts +++ b/src/getFunctionDeclarationFromCall.ts @@ -6,8 +6,17 @@ export function getFunctionDeclarationFromCall( node: ts.CallExpression, typeChecker: ts.TypeChecker, ) { - let declaration = typeChecker.getSymbolAtLocation(node.expression) - ?.valueDeclaration; + let declaration: ts.Node | undefined = typeChecker.getSymbolAtLocation( + node.expression, + )?.valueDeclaration; + + if (!declaration) { + return undefined; + } + + if (ts.isVariableDeclaration(declaration)) { + declaration = declaration.initializer; + } if (!declaration) { return undefined; diff --git a/src/isFunctionDeclarationWithBody.ts b/src/isFunctionDeclarationWithBody.ts index e8b3bfa..1fed3be 100644 --- a/src/isFunctionDeclarationWithBody.ts +++ b/src/isFunctionDeclarationWithBody.ts @@ -6,7 +6,9 @@ export const isFunctionWithBody = ( node: ts.Node, ): node is FunctionLikeWithBody => { return ( - (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) && + (ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isMethodDeclaration(node)) && !!node.body ); }; diff --git a/src/transformerProgram.test.ts b/src/transformerProgram.test.ts index 97494a5..7b6873e 100644 --- a/src/transformerProgram.test.ts +++ b/src/transformerProgram.test.ts @@ -58,6 +58,15 @@ function expectResultToBe(actual: string, expected: string) { ); } +function expectResultToEndWith(actual: string, expected: string) { + const normalized = normalizeFile(actual); + + expect(normalized.indexOf(expected)).toBeCloseTo( + normalized.length - expected.length, + -1, + ); +} + describe("transformerProgram", () => { describe("function contents", () => { test("BinaryExpression", () => { @@ -65,7 +74,7 @@ describe("transformerProgram", () => { function addToStringLength(base: string) { return base.length + 3; } - + addToStringLength("abc"); `); @@ -75,7 +84,7 @@ describe("transformerProgram", () => { function addToStringLength(base) { return base.length + 3; } - + "abc".length + 3; `, ); @@ -86,7 +95,7 @@ describe("transformerProgram", () => { function incrementCount(count: number) { return count++; } - + const value = 123; incrementCount(value); `); @@ -97,7 +106,7 @@ describe("transformerProgram", () => { function incrementCount(count) { return count++; } - + const value = 123; value++; `, @@ -109,7 +118,7 @@ describe("transformerProgram", () => { function isNotEmpty(text: string) { return !!text.length; } - + isNotEmpty("Boo! 👻"); `); @@ -119,14 +128,14 @@ describe("transformerProgram", () => { function isNotEmpty(text) { return !!text.length; } - + !!"Boo! 👻".length; `, ); }); }); - test("function kind", () => { + describe("function kinds", () => { test("FunctionExpression in object property", () => { const result = getResult(` const Utils = { @@ -134,40 +143,29 @@ describe("transformerProgram", () => { return !!text.length; } } - + Utils.isNotEmpty("Boo! 👻"); `); - expectResultToBe( - result, - ` - const Utils = { - isNotEmpty: function (text) { - return !!text.length; - } - } - - !!"Boo! 👻".length; - `, - ); + expectResultToEndWith(result, `!!"Boo! 👻".length;`); }); test("FunctionExpression in variable", () => { const result = getResult(` - const isNotEmpty = function (text: string) { + const isTextNotEmpty = function (text: string) { return !!text.length; - } - - isNotEmpty("Boo! 👻"); + }; + + isTextNotEmpty("Boo! 👻"); `); expectResultToBe( result, ` - const isNotEmpty = function (text) { + const isTextNotEmpty = function (text) { return !!text.length; - } - + }; + !!"Boo! 👻".length; `, ); @@ -196,4 +194,182 @@ describe("transformerProgram", () => { ); }); }); + + describe("size thresholds", () => { + describe("FunctionExpressions: inline", () => { + test("longer name than body", () => { + const result = getResult(` + const Utils = { + longerName: function longerName(a: number) { + return a + 1.2; + }, + }; + + Utils.longerName(3); + `); + + expectResultToEndWith(result, "3 + 1.2"); + }); + + test("shorter name than body", () => { + const result = getResult(` + const Utils = { + shorterName: function shorterName(a: number) { + return a + 1.000000000000000002; + }, + }; + + Utils.shorterName(3); + `); + + expectResultToEndWith(result, "Utils.shorterName(3);"); + }); + }); + + describe("FunctionExpressions: method", () => { + test("longer name than body", () => { + const result = getResult(` + const Utils = { + longerName(a: number) { + return a + 1.2; + }; + } + + Utils.longerName(3); + `); + + expectResultToEndWith(result, `3 + 1.2;`); + }); + + test("shorter name than body", () => { + const result = getResult(` + const Utils = { + shortName(a: number) { + return a + 1.000000000000000002; + } + }; + + Utils.shortName(3); + `); + + expectResultToEndWith(result, `Utils.shortName(3);`); + }); + }); + + describe("FunctionExpressions: property", () => { + test("longer name than body", () => { + const result = getResult(` + const Utils = { + longerName: function (a: number) { + return a + 1.2; + } + }; + + Utils.longerName(3); + `); + + expectResultToEndWith(result, `3 + 1.2;`); + }); + + test("shorter name than body", () => { + const result = getResult(` + const Utils = { + shortName: function(a: number) { + return a + 1.000000000000000002; + } + }; + + Utils.shortName(3); + `); + + expectResultToEndWith(result, `Utils.shortName(3);`); + }); + }); + + describe("property FunctionDeclarations", () => { + test("longer name than body", () => { + const result = getResult(` + function longerName(a: number) { + return a + 1.2; + } + + const Utils = { longerName: longerName }; + + Utils.longerName(3); + `); + + expectResultToEndWith(result, `3 + 1.2;`); + }); + + test("shorter name than body", () => { + const result = getResult(` + function shorterName(a: number) { + return a + 1.000000000000000002; + } + + const Utils = { shorterName: shorterName }; + + Utils.shorterName(3); + `); + + expectResultToEndWith(result, `Utils.shorterName(3);`); + }); + }); + + describe("shorthand property FunctionDeclarations", () => { + test("longer name than body", () => { + const result = getResult(` + function longerName(a: number) { + return a + 1.2; + } + + const Utils = { longerName }; + + Utils.longerName(3); + `); + + expectResultToEndWith(result, `3 + 1.2;`); + }); + + test("shorter name than body", () => { + const result = getResult(` + function shorterName(a: number) { + return a + 1.000000000000000002; + } + + const Utils = { shorterName }; + + Utils.shorterName(3); + `); + + expectResultToEndWith(result, `Utils.shorterName(3);`); + }); + }); + + describe("standalone FunctionDeclarations", () => { + test("longer name than body", () => { + const result = getResult(` + function longerName(a: number) { + return a + 1.2; + } + + longerName(3); + `); + + expectResultToEndWith(result, `3 + 1.2;`); + }); + + test("shorter name than body", () => { + const result = getResult(` + function shorterName(a: number) { + return a + 1.000000000000000002; + } + + shorterName(3); + `); + + expectResultToEndWith(result, `shorterName(3);`); + }); + }); + }); }); diff --git a/src/transformerProgram.ts b/src/transformerProgram.ts index 279211c..d468012 100644 --- a/src/transformerProgram.ts +++ b/src/transformerProgram.ts @@ -1,7 +1,9 @@ +import { CachedFactory } from "cached-factory"; import ts from "typescript"; import { getFunctionDeclarationFromCall } from "./getFunctionDeclarationFromCall.js"; import { transformToInline } from "./transformToInline.js"; +import { SmallFunctionLikeWithBody } from "./types.js"; export const transformerProgram = (program: ts.Program) => { const transformerFactory: ts.TransformerFactory = ( @@ -10,10 +12,31 @@ export const transformerProgram = (program: ts.Program) => { return (sourceFile) => { const typeChecker = program.getTypeChecker(); + const functionDeclarationLengths = new CachedFactory( + (node: SmallFunctionLikeWithBody) => + node.body.statements[0].getText(sourceFile).length - "return".length, + ); + + const getFunctionDeclarationForReplacement = ( + node: ts.CallExpression, + ) => { + const functionDeclaration = getFunctionDeclarationFromCall( + node, + typeChecker, + ); + + return ( + functionDeclaration && + functionDeclarationLengths.get(functionDeclaration) <= + node.getText(sourceFile).length && + functionDeclaration + ); + }; + const visitor = (node: ts.Node): ts.Node => { const functionDeclaration = ts.isCallExpression(node) && - getFunctionDeclarationFromCall(node, typeChecker); + getFunctionDeclarationForReplacement(node); if (functionDeclaration) { return transformToInline(node, functionDeclaration, context);