Skip to content

Commit 6174791

Browse files
committed
refactor: preparing using of extension from imported references
1 parent 74ffbe9 commit 6174791

15 files changed

Lines changed: 342 additions & 99 deletions

.github/workflows/test-coverage.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
node-version: [10.x]
15+
node-version: [22.x]
1616
steps:
1717
- uses: actions/checkout@v2
1818
- name: Use Node.js ${{ matrix.node-version }}
19-
uses: actions/setup-node@v1
19+
uses: actions/setup-node@v4
2020
with:
2121
node-version: ${{ matrix.node-version }}
2222
- name: install
2323
run: npm ci
24-
- name: "Test"
24+
- name: 'Test'
2525
uses: paambaati/codeclimate-action@v2.6.0
2626
env:
2727
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ class StringExtensions {
1919
}
2020
}
2121

22-
interface String {
23-
size(): number;
22+
declare global {
23+
interface String {
24+
size(): number;
25+
}
2426
}
2527

2628
'my-string'.size();
@@ -35,7 +37,7 @@ Also, this library works with two modes:
3537

3638
Each of them are explained below and you can use the one that fits you better!
3739

38-
# AST Transformer mode
40+
# AST Transformer mode (experimental)
3941

4042
In this mode, everything is done at transpiling time, making you able to access each of the extensions methods just by importing them where you need, like this:
4143

src/plugin/emitter.ts

Lines changed: 64 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,94 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import * as ts from 'typescript';
3-
4-
function createProgramAndGetTypeChecker(context: ts.TransformationContext) {
5-
const compilerOptions = context.getCompilerOptions();
6-
const rootDir = compilerOptions.rootDir || '.';
7-
// Create a TypeScript program with the transformed source files
8-
const program = ts.createProgram({
9-
options: compilerOptions,
10-
rootNames: [rootDir],
11-
});
12-
13-
// Get the TypeChecker from the program
14-
const typeChecker = program.getTypeChecker();
15-
16-
return { program, typeChecker };
17-
}
18-
19-
// Function to find the class where the method belongs
20-
function findClassForMethod(
21-
methodNode: ts.MethodDeclaration,
22-
): ts.ClassDeclaration | undefined {
23-
let parent: ts.Node = methodNode.parent;
24-
while (parent) {
25-
if (ts.isClassDeclaration(parent)) {
26-
return parent; // Found the class
27-
}
28-
parent = parent.parent; // Keep looking up the tree
29-
}
30-
return undefined; // No class found (in case it's not part of a class)
31-
}
3+
import {
4+
findClassForMethod,
5+
createProgramAndGetTypeChecker,
6+
traverseImportFactoryBuilder,
7+
MapEx,
8+
} from './helpers';
9+
import { registerReferencedExtensions } from './helpers/register-referenced-extensions';
3210

3311
export function before() {
12+
const extensions = new MapEx<
13+
ts.SourceFile,
14+
MapEx<ts.Type, MapEx<string, ts.Identifier>>
15+
>();
16+
const sources = new MapEx<string, Set<ts.SourceFile>>();
17+
const sourceNameMap = new Map<string, ts.SourceFile>();
3418
return (context: ts.TransformationContext) => {
35-
const { typeChecker } = createProgramAndGetTypeChecker(context);
36-
const extensions = new Map<
37-
ts.SourceFile,
38-
Map<ts.Type, Map<string, ts.Identifier>>
39-
>();
40-
return (rootNode: ts.SourceFile) => {
41-
const registerExtensions: ts.Visitor = (node: ts.Node): ts.Node => {
19+
const tsRef = createProgramAndGetTypeChecker(context);
20+
const { traverseImportFactory } = traverseImportFactoryBuilder(
21+
extensions,
22+
sources,
23+
tsRef,
24+
);
25+
26+
return function transformExtensionRefs(rootNode: ts.SourceFile) {
27+
const { getExtensionCall, traverseImport } = traverseImportFactory(
28+
rootNode,
29+
sourceNameMap,
30+
);
31+
32+
/**
33+
* Traverse function to register every extension the
34+
* declared
35+
* @param node The node to be analyzed
36+
*/
37+
function registerExtensions(node: ts.Node): ts.Node {
4238
const visitNext = () =>
4339
ts.visitEachChild(node, registerExtensions, context);
40+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
41+
const visited = visitNext();
42+
registerReferencedExtensions(sources, node, rootNode, tsRef);
43+
return visited;
44+
}
4445
const decorators = ts.canHaveDecorators(node)
4546
? ts.getDecorators(node)
4647
: undefined;
4748
// Handle method declarations with the @ExtensionMethod decorator
48-
if (!ts.isMethodDeclaration(node) || !decorators?.length) {
49-
return visitNext();
50-
}
49+
if (!ts.isMethodDeclaration(node)) return visitNext();
5150
// Check for the @ExtensionMethod decorator
52-
const extensionDecorator = decorators.find(
51+
const extensionDecorator = decorators?.find(
5352
(decorator) => decorator.getText() === '@ExtensionMethod',
5453
);
54+
const first = node.parameters[0];
55+
const type = first
56+
? tsRef.typeChecker.getTypeAtLocation(first)
57+
: undefined;
58+
const cls = findClassForMethod(node);
5559

56-
if (!extensionDecorator) return visitNext();
57-
// Ensure the method is static and has 'this' parameter for the extension type
5860
if (
61+
!extensionDecorator ||
5962
!node.parameters.length ||
6063
!node.modifiers?.some(
6164
(mod) => mod.kind === ts.SyntaxKind.StaticKeyword,
62-
)
65+
) ||
66+
!type ||
67+
!cls?.name
6368
) {
6469
return visitNext();
6570
}
66-
const first = node.parameters[0];
67-
if (!first) return visitNext();
68-
const type = typeChecker.getTypeAtLocation(first);
69-
if (!type) return visitNext();
70-
let sourceMap = extensions.get(rootNode);
71-
if (!sourceMap) {
72-
sourceMap = new Map();
73-
extensions.set(rootNode, sourceMap);
74-
}
75-
let extensionMethods = sourceMap.get(type);
76-
if (!extensionMethods) {
77-
extensionMethods = new Map();
78-
sourceMap.set(type, extensionMethods);
79-
}
80-
const cls = findClassForMethod(node);
81-
if (!cls?.name) return visitNext();
82-
extensionMethods.set(node.name.getText(), cls.name);
71+
extensions
72+
.getOrSet(rootNode, () => new MapEx())
73+
.getOrSet(type, () => new MapEx())
74+
.set(node.name.getText(), cls.name);
75+
sources.getOrSet(rootNode.fileName, () => new Set()).add(rootNode);
76+
sourceNameMap.set(rootNode.fileName, rootNode);
8377
return ts.visitEachChild(node, registerExtensions, context);
84-
};
78+
}
8579

86-
const transformExtensions: ts.Visitor = (node: ts.Node): ts.Node => {
80+
/**
81+
* Traverse function the replace every extension import
82+
* and every extension method call to static call reference
83+
* @param node the node to be analyzed
84+
*/
85+
function transformExtensions(node: ts.Node): ts.Node {
8786
const visitNext = () =>
8887
ts.visitEachChild(node, transformExtensions, context);
88+
if (ts.isImportDeclaration(node)) return traverseImport(node);
8989
if (!ts.isCallExpression(node)) return visitNext();
90-
const { expression, arguments: args } = node;
91-
if (!ts.isPropertyAccessExpression(expression)) return visitNext();
92-
const targetInstance = expression.expression;
93-
const methodName = expression.name.getText();
94-
if (!targetInstance || !methodName) return visitNext();
95-
const extensionList = extensions.get(rootNode);
96-
if (!extensionList) return visitNext();
97-
const type = typeChecker.getTypeAtLocation(targetInstance);
98-
if (!type) return visitNext();
99-
let extension = extensionList.get(type)?.get(methodName);
100-
if (!extension) {
101-
for (const [key, value] of extensionList.entries()) {
102-
if (typeChecker.isTypeAssignableTo(type, key)) {
103-
extension = value.get(methodName);
104-
if (extension) break;
105-
}
106-
}
107-
}
108-
if (!extension) return visitNext();
109-
110-
// Create the transformed call: MyExtensionClass.myExtensionMethod(myInstance)
111-
return ts.factory.createCallExpression(
112-
ts.factory.createPropertyAccessExpression(
113-
extension,
114-
ts.factory.createIdentifier(methodName),
115-
),
116-
undefined,
117-
[targetInstance, ...args],
118-
);
119-
};
90+
return getExtensionCall(node) ?? visitNext();
91+
}
12092

12193
ts.visitNode(rootNode, registerExtensions);
12294

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as ts from 'typescript';
2+
3+
export function createImportAsDeclaration(
4+
moduleSpecifier: ts.Expression,
5+
alias: string,
6+
): ts.ImportDeclaration {
7+
const importClause = ts.factory.createImportClause(
8+
false,
9+
ts.factory.createIdentifier(alias),
10+
undefined,
11+
);
12+
13+
return ts.factory.createImportDeclaration(
14+
undefined,
15+
importClause,
16+
moduleSpecifier,
17+
);
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as ts from 'typescript';
2+
import { TsRef } from './ts-ref';
3+
4+
export function createProgramAndGetTypeChecker(
5+
context: ts.TransformationContext,
6+
): TsRef {
7+
const compilerOptions = context.getCompilerOptions();
8+
const rootDir = compilerOptions.rootDir || '.';
9+
// Create a TypeScript program with the transformed source files
10+
const program = ts.createProgram({
11+
options: compilerOptions,
12+
rootNames: [rootDir],
13+
});
14+
15+
// Get the TypeChecker from the program
16+
const typeChecker = program.getTypeChecker();
17+
18+
return { program, typeChecker };
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as ts from 'typescript';
2+
3+
// Function to find the class where the method belongs
4+
export function findClassForMethod(
5+
methodNode: ts.MethodDeclaration,
6+
): ts.ClassDeclaration | undefined {
7+
let parent: ts.Node = methodNode.parent;
8+
while (parent) {
9+
if (ts.isClassDeclaration(parent)) {
10+
return parent; // Found the class
11+
}
12+
parent = parent.parent; // Keep looking up the tree
13+
}
14+
return undefined; // No class found (in case it's not part of a class)
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import ts = require('typescript');
2+
3+
export function getExtensionFromMap(
4+
typeChecker: ts.TypeChecker,
5+
extensionList: Map<ts.Type, Map<string, ts.Identifier>>,
6+
methodName: string,
7+
type: ts.Type,
8+
) {
9+
for (const [key, value] of extensionList.entries()) {
10+
if (typeChecker.isTypeAssignableTo(type, key)) {
11+
const extension = value.get(methodName);
12+
if (extension) return extension;
13+
}
14+
}
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import ts = require('typescript');
2+
3+
export function* getExtensions(
4+
sources: Map<string, Set<ts.SourceFile>>,
5+
extensions: Map<ts.SourceFile, Map<ts.Type, Map<string, ts.Identifier>>>,
6+
rootNode: ts.SourceFile,
7+
importRefs: Map<string, ts.Identifier>,
8+
) {
9+
let extensionList = extensions.get(rootNode);
10+
if (extensionList) yield { extensionList, identifier: undefined };
11+
const list = sources.get(rootNode.fileName);
12+
if (!list) return;
13+
for (const item of list) {
14+
extensionList = extensions.get(item);
15+
if (extensionList) {
16+
yield {
17+
extensionList,
18+
identifier: importRefs.get(item.fileName),
19+
};
20+
}
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as ts from 'typescript';
2+
3+
export function getImportFileName(
4+
program: ts.Program,
5+
importNode: ts.ImportDeclaration | ts.ExportDeclaration,
6+
currentFile: ts.SourceFile,
7+
) {
8+
const moduleSpecifier = importNode.moduleSpecifier;
9+
10+
if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) {
11+
return undefined;
12+
}
13+
const importPath = moduleSpecifier.text;
14+
const resolvedModule = ts.resolveModuleName(
15+
importPath,
16+
currentFile.fileName,
17+
program.getCompilerOptions(),
18+
ts.sys,
19+
).resolvedModule;
20+
const fileName = resolvedModule?.resolvedFileName;
21+
if (!fileName) return undefined;
22+
return fileName;
23+
}

src/plugin/helpers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export * from './create-import-as-declaration';
2+
export * from './create-program-nd-get-type-checker';
3+
export * from './find-class-for-method';
4+
export * from './get-extension-from-map';
5+
export * from './get-extensions';
6+
export * from './get-import-file-names';
7+
export * from './map-ex';
8+
export * from './register-referenced-extensions';
9+
export * from './traverse-import-factory-builder';
10+
export * from './ts-ref';

0 commit comments

Comments
 (0)