boperators 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/{esm/core → core}/OverloadInjector.d.ts +3 -3
- package/dist/core/OverloadInjector.js +154 -0
- package/dist/core/SourceMap.d.ts +20 -0
- package/dist/{cjs/core → core}/SourceMap.js +2 -78
- package/dist/core/v3SourceMap.d.ts +21 -0
- package/dist/core/v3SourceMap.js +121 -0
- package/dist/{esm/index.d.ts → index.d.ts} +3 -1
- package/dist/{cjs/index.js → index.js} +4 -2
- package/package.json +14 -12
- package/dist/cjs/core/OverloadInjector.js +0 -136
- package/dist/cjs/lib/index.js +0 -17
- package/dist/esm/core/BopConfig.js +0 -65
- package/dist/esm/core/ErrorManager.js +0 -118
- package/dist/esm/core/OverloadInjector.js +0 -129
- package/dist/esm/core/OverloadStore.js +0 -624
- package/dist/esm/core/SourceMap.d.ts +0 -41
- package/dist/esm/core/SourceMap.js +0 -164
- package/dist/esm/core/helpers/ensureImportedName.js +0 -46
- package/dist/esm/core/helpers/getImportedNameForSymbol.js +0 -60
- package/dist/esm/core/helpers/getModuleSpecifier.js +0 -57
- package/dist/esm/core/helpers/getOperatorStringFromProperty.js +0 -30
- package/dist/esm/core/helpers/resolveExpressionType.js +0 -37
- package/dist/esm/core/helpers/unwrapInitializer.js +0 -15
- package/dist/esm/core/operatorMap.js +0 -92
- package/dist/esm/core/validateExports.js +0 -84
- package/dist/esm/index.js +0 -14
- package/dist/esm/lib/index.d.ts +0 -1
- package/dist/esm/lib/index.js +0 -1
- package/dist/esm/lib/operatorSymbols.js +0 -36
- /package/dist/{esm/core → core}/BopConfig.d.ts +0 -0
- /package/dist/{cjs/core → core}/BopConfig.js +0 -0
- /package/dist/{esm/core → core}/ErrorManager.d.ts +0 -0
- /package/dist/{cjs/core → core}/ErrorManager.js +0 -0
- /package/dist/{esm/core → core}/OverloadStore.d.ts +0 -0
- /package/dist/{cjs/core → core}/OverloadStore.js +0 -0
- /package/dist/{esm/core → core}/helpers/ensureImportedName.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/ensureImportedName.js +0 -0
- /package/dist/{esm/core → core}/helpers/getImportedNameForSymbol.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/getImportedNameForSymbol.js +0 -0
- /package/dist/{esm/core → core}/helpers/getModuleSpecifier.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/getModuleSpecifier.js +0 -0
- /package/dist/{esm/core → core}/helpers/getOperatorStringFromProperty.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/getOperatorStringFromProperty.js +0 -0
- /package/dist/{esm/core → core}/helpers/resolveExpressionType.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/resolveExpressionType.js +0 -0
- /package/dist/{esm/core → core}/helpers/unwrapInitializer.d.ts +0 -0
- /package/dist/{cjs/core → core}/helpers/unwrapInitializer.js +0 -0
- /package/dist/{esm/core → core}/operatorMap.d.ts +0 -0
- /package/dist/{cjs/core → core}/operatorMap.js +0 -0
- /package/dist/{esm/core → core}/validateExports.d.ts +0 -0
- /package/dist/{cjs/core → core}/validateExports.js +0 -0
- /package/dist/{esm/lib → lib}/operatorSymbols.d.ts +0 -0
- /package/dist/{cjs/lib → lib}/operatorSymbols.js +0 -0
package/README.md
CHANGED
|
@@ -11,7 +11,13 @@ Operator overloading is a common programming feature that JavaScript lacks. Just
|
|
|
11
11
|
|
|
12
12
|
`boperators` finally brings operator overloading to JavaScript by leveraging TypeScript typings. You define one or more overload functions on a class for whichever operators you want, and with magic we search for anywhere you've used overloaded operators and substitute in your functions.
|
|
13
13
|
|
|
14
|
-
This is the core library and API, and isn't designed to be used directly. Instead, you can use
|
|
14
|
+
This is the core library and API, and isn't designed to be used directly. Instead, you can use:
|
|
15
|
+
- The [Boperators CLI](https://www.npmjs.com/package/@boperators/cli);
|
|
16
|
+
- The [`tsc`](https://www.npmjs.com/package/@boperators/plugin-tsc) plugin;
|
|
17
|
+
- The [NextJS/Webpack loader](https://www.npmjs.com/package/@boperators/webpack-loader);
|
|
18
|
+
- The [Vite plugin](https://www.npmjs.com/package/@boperators/plugin-vite);
|
|
19
|
+
- The [ESBuild plugin](https://www.npmjs.com/package/@boperators/plugin-esbuild);
|
|
20
|
+
- The [Bun plugin](https://www.npmjs.com/package/@boperators/plugin-bun) for running directly with Bun.
|
|
15
21
|
|
|
16
22
|
We also offer a [TypeScript Language Server plugin](https://www.npmjs.com/package/@boperators/plugin-ts-language-server) for real-time type hinting and intellisense in your IDE, and an [MCP server](https://www.npmjs.com/package/@boperators/mcp-server) to optimize your vibe coding experience.
|
|
17
23
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { SourceFile, type Project as TsMorphProject } from "ts-morph";
|
|
2
2
|
import type { BopLogger } from "./BopConfig";
|
|
3
3
|
import type { OverloadStore } from "./OverloadStore";
|
|
4
|
-
import {
|
|
4
|
+
import { type EditRecord } from "./SourceMap";
|
|
5
5
|
export type TransformResult = {
|
|
6
6
|
/** The mutated ts-morph SourceFile (same reference as input). */
|
|
7
7
|
sourceFile: SourceFile;
|
|
8
8
|
/** The full text after transformation. */
|
|
9
9
|
text: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** Edit records mapping positions between original and transformed text. */
|
|
11
|
+
edits: readonly EditRecord[];
|
|
12
12
|
};
|
|
13
13
|
export declare class OverloadInjector {
|
|
14
14
|
/**
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OverloadInjector = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const ts_morph_1 = require("ts-morph");
|
|
9
|
+
const ensureImportedName_1 = require("./helpers/ensureImportedName");
|
|
10
|
+
const getModuleSpecifier_1 = require("./helpers/getModuleSpecifier");
|
|
11
|
+
const resolveExpressionType_1 = require("./helpers/resolveExpressionType");
|
|
12
|
+
const operatorMap_1 = require("./operatorMap");
|
|
13
|
+
const SourceMap_1 = require("./SourceMap");
|
|
14
|
+
class OverloadInjector {
|
|
15
|
+
constructor(
|
|
16
|
+
/**
|
|
17
|
+
* TS Morph project.
|
|
18
|
+
*/
|
|
19
|
+
_project,
|
|
20
|
+
/**
|
|
21
|
+
* Overload store.
|
|
22
|
+
*/
|
|
23
|
+
_overloadStore, logger) {
|
|
24
|
+
this._project = _project;
|
|
25
|
+
this._overloadStore = _overloadStore;
|
|
26
|
+
this._logger = logger;
|
|
27
|
+
}
|
|
28
|
+
overloadFile(file) {
|
|
29
|
+
const sourceFile = file instanceof ts_morph_1.SourceFile
|
|
30
|
+
? file
|
|
31
|
+
: this._project.getSourceFileOrThrow(file);
|
|
32
|
+
const fileName = node_path_1.default.basename(sourceFile.getFilePath());
|
|
33
|
+
const originalText = sourceFile.getFullText();
|
|
34
|
+
let transformCount = 0;
|
|
35
|
+
// Outer loop: re-run all three passes until the file is fully stable.
|
|
36
|
+
// This is necessary because transforming a unary expression can unblock
|
|
37
|
+
// a binary expression (e.g. in `-v1 + v2`, the unary `-` must be
|
|
38
|
+
// replaced first so TypeScript can resolve its return type; the binary
|
|
39
|
+
// `+` overload then becomes matchable on the next outer iteration).
|
|
40
|
+
let outerChanged = true;
|
|
41
|
+
while (outerChanged) {
|
|
42
|
+
outerChanged = false;
|
|
43
|
+
// ── Binary expressions ────────────────────────────────────────────
|
|
44
|
+
// Process one innermost expression per inner iteration, re-fetching
|
|
45
|
+
// descendants each time so types resolve correctly after each
|
|
46
|
+
// transformation and AST references stay fresh.
|
|
47
|
+
let changed = true;
|
|
48
|
+
while (changed) {
|
|
49
|
+
changed = false;
|
|
50
|
+
const binaryExpressions = sourceFile
|
|
51
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)
|
|
52
|
+
.reverse();
|
|
53
|
+
for (const expression of binaryExpressions) {
|
|
54
|
+
const operatorKind = expression.getOperatorToken().getKind();
|
|
55
|
+
if (!(0, operatorMap_1.isOperatorSyntaxKind)(operatorKind))
|
|
56
|
+
continue;
|
|
57
|
+
const lhs = expression.getLeft();
|
|
58
|
+
const leftType = (0, resolveExpressionType_1.resolveExpressionType)(lhs);
|
|
59
|
+
const rhs = expression.getRight();
|
|
60
|
+
const rightType = (0, resolveExpressionType_1.resolveExpressionType)(rhs);
|
|
61
|
+
const overloadDesc = this._overloadStore.findOverload(operatorKind, leftType, rightType);
|
|
62
|
+
if (!overloadDesc)
|
|
63
|
+
continue;
|
|
64
|
+
const { className: classNameRaw, classFilePath, operatorString, index, isStatic, } = overloadDesc;
|
|
65
|
+
// Look up the fresh ClassDeclaration from the project
|
|
66
|
+
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
67
|
+
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
68
|
+
// Ensure class is imported, get its textual name
|
|
69
|
+
const classSymbol = classDecl.getSymbol();
|
|
70
|
+
if (!classSymbol)
|
|
71
|
+
throw new Error(`No symbol for class "${classNameRaw}"`);
|
|
72
|
+
const classModuleSpecifier = (0, getModuleSpecifier_1.getModuleSpecifier)(sourceFile, classSourceFile);
|
|
73
|
+
const className = (0, ensureImportedName_1.ensureImportedName)(sourceFile, classSymbol, classModuleSpecifier);
|
|
74
|
+
// Build the text code to replace the binary operator with the overload call
|
|
75
|
+
const overloadCall = isStatic
|
|
76
|
+
? `${className}["${operatorString}"][${index}](${lhs.getText()}, ${rhs.getText()})`
|
|
77
|
+
: `${lhs.getText()}["${operatorString}"][${index}].call(${lhs.getText()}, ${rhs.getText()})`;
|
|
78
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
79
|
+
expression.replaceWithText(overloadCall);
|
|
80
|
+
transformCount++;
|
|
81
|
+
changed = true;
|
|
82
|
+
outerChanged = true;
|
|
83
|
+
break; // re-fetch descendants after each mutation
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── Prefix unary expressions (-x, +x, !x, ~x) ───────────────────
|
|
87
|
+
changed = true;
|
|
88
|
+
while (changed) {
|
|
89
|
+
changed = false;
|
|
90
|
+
const prefixExpressions = sourceFile
|
|
91
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.PrefixUnaryExpression)
|
|
92
|
+
.reverse();
|
|
93
|
+
for (const expression of prefixExpressions) {
|
|
94
|
+
const operatorKind = expression.getOperatorToken();
|
|
95
|
+
if (!(0, operatorMap_1.isPrefixUnaryOperatorSyntaxKind)(operatorKind))
|
|
96
|
+
continue;
|
|
97
|
+
const operand = expression.getOperand();
|
|
98
|
+
const operandType = (0, resolveExpressionType_1.resolveExpressionType)(operand);
|
|
99
|
+
const overloadDesc = this._overloadStore.findPrefixUnaryOverload(operatorKind, operandType);
|
|
100
|
+
if (!overloadDesc)
|
|
101
|
+
continue;
|
|
102
|
+
const { className: classNameRaw, classFilePath, operatorString, index, } = overloadDesc;
|
|
103
|
+
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
104
|
+
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
105
|
+
const classSymbol = classDecl.getSymbol();
|
|
106
|
+
if (!classSymbol)
|
|
107
|
+
throw new Error(`No symbol for class "${classNameRaw}"`);
|
|
108
|
+
const classModuleSpecifier = (0, getModuleSpecifier_1.getModuleSpecifier)(sourceFile, classSourceFile);
|
|
109
|
+
const className = (0, ensureImportedName_1.ensureImportedName)(sourceFile, classSymbol, classModuleSpecifier);
|
|
110
|
+
const overloadCall = `${className}["${operatorString}"][${index}](${operand.getText()})`;
|
|
111
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
112
|
+
expression.replaceWithText(overloadCall);
|
|
113
|
+
transformCount++;
|
|
114
|
+
changed = true;
|
|
115
|
+
outerChanged = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ── Postfix unary expressions (x++, x--) ─────────────────────────
|
|
120
|
+
changed = true;
|
|
121
|
+
while (changed) {
|
|
122
|
+
changed = false;
|
|
123
|
+
const postfixExpressions = sourceFile
|
|
124
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.PostfixUnaryExpression)
|
|
125
|
+
.reverse();
|
|
126
|
+
for (const expression of postfixExpressions) {
|
|
127
|
+
const operatorKind = expression.getOperatorToken();
|
|
128
|
+
if (!(0, operatorMap_1.isPostfixUnaryOperatorSyntaxKind)(operatorKind))
|
|
129
|
+
continue;
|
|
130
|
+
const operand = expression.getOperand();
|
|
131
|
+
const operandType = (0, resolveExpressionType_1.resolveExpressionType)(operand);
|
|
132
|
+
const overloadDesc = this._overloadStore.findPostfixUnaryOverload(operatorKind, operandType);
|
|
133
|
+
if (!overloadDesc)
|
|
134
|
+
continue;
|
|
135
|
+
const { operatorString, index } = overloadDesc;
|
|
136
|
+
const overloadCall = `${operand.getText()}["${operatorString}"][${index}].call(${operand.getText()})`;
|
|
137
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
138
|
+
expression.replaceWithText(overloadCall);
|
|
139
|
+
transformCount++;
|
|
140
|
+
changed = true;
|
|
141
|
+
outerChanged = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (transformCount > 0) {
|
|
147
|
+
this._logger.debug(`${fileName}: ${transformCount} expression${transformCount === 1 ? "" : "s"} transformed`);
|
|
148
|
+
}
|
|
149
|
+
const text = sourceFile.getFullText();
|
|
150
|
+
const edits = (0, SourceMap_1.computeEdits)(originalText, text);
|
|
151
|
+
return { sourceFile, text, edits };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.OverloadInjector = OverloadInjector;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type EditRecord = {
|
|
2
|
+
/** Start position in the original source */
|
|
3
|
+
origStart: number;
|
|
4
|
+
/** End position (exclusive) in the original source */
|
|
5
|
+
origEnd: number;
|
|
6
|
+
/** Start position in the transformed source */
|
|
7
|
+
transStart: number;
|
|
8
|
+
/** End position (exclusive) in the transformed source */
|
|
9
|
+
transEnd: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Compute edit records by scanning both texts for mismatches.
|
|
13
|
+
* @public
|
|
14
|
+
*
|
|
15
|
+
* Uses character-level comparison with anchor-based convergence:
|
|
16
|
+
* identical characters are skipped, mismatches start an edit region,
|
|
17
|
+
* and convergence is found by searching for a matching anchor substring
|
|
18
|
+
* in the remaining text.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeEdits(original: string, transformed: string): EditRecord[];
|
|
@@ -1,85 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
/**
|
|
5
|
-
* Bidirectional position mapping between original and transformed source text.
|
|
6
|
-
*
|
|
7
|
-
* Computes a list of edit records by diffing the two texts, then provides
|
|
8
|
-
* O(edits) position and span mapping in both directions.
|
|
9
|
-
*/
|
|
10
|
-
class SourceMap {
|
|
11
|
-
constructor(original, transformed) {
|
|
12
|
-
this.edits = computeEdits(original, transformed);
|
|
13
|
-
}
|
|
14
|
-
/** Returns true if no edits were detected (original === transformed). */
|
|
15
|
-
get isEmpty() {
|
|
16
|
-
return this.edits.length === 0;
|
|
17
|
-
}
|
|
18
|
-
/** Map a position from original source to transformed source. */
|
|
19
|
-
originalToTransformed(pos) {
|
|
20
|
-
let delta = 0;
|
|
21
|
-
for (const edit of this.edits) {
|
|
22
|
-
if (pos < edit.origStart) {
|
|
23
|
-
// Before this edit — just apply accumulated delta
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
if (pos < edit.origEnd) {
|
|
27
|
-
// Inside an edited region — map to start of the transformed replacement
|
|
28
|
-
return edit.transStart;
|
|
29
|
-
}
|
|
30
|
-
// Past this edit — accumulate the delta
|
|
31
|
-
delta = edit.transEnd - edit.origEnd;
|
|
32
|
-
}
|
|
33
|
-
return pos + delta;
|
|
34
|
-
}
|
|
35
|
-
/** Map a position from transformed source to original source. */
|
|
36
|
-
transformedToOriginal(pos) {
|
|
37
|
-
let delta = 0;
|
|
38
|
-
for (const edit of this.edits) {
|
|
39
|
-
if (pos < edit.transStart) {
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
if (pos < edit.transEnd) {
|
|
43
|
-
// Inside a transformed region — map to start of the original span
|
|
44
|
-
return edit.origStart;
|
|
45
|
-
}
|
|
46
|
-
delta = edit.origEnd - edit.transEnd;
|
|
47
|
-
}
|
|
48
|
-
return pos + delta;
|
|
49
|
-
}
|
|
50
|
-
/** Map a text span { start, length } from transformed positions to original positions. */
|
|
51
|
-
remapSpan(span) {
|
|
52
|
-
const origStart = this.transformedToOriginal(span.start);
|
|
53
|
-
const origEnd = this.transformedToOriginal(span.start + span.length);
|
|
54
|
-
return { start: origStart, length: origEnd - origStart };
|
|
55
|
-
}
|
|
56
|
-
/** Check if an original-source position falls inside an edited region. */
|
|
57
|
-
isInsideEdit(originalPos) {
|
|
58
|
-
for (const edit of this.edits) {
|
|
59
|
-
if (originalPos < edit.origStart)
|
|
60
|
-
return false;
|
|
61
|
-
if (originalPos < edit.origEnd)
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* For a position inside an edited region (in original coords),
|
|
68
|
-
* return the EditRecord it falls in, or undefined.
|
|
69
|
-
*/
|
|
70
|
-
getEditAt(originalPos) {
|
|
71
|
-
for (const edit of this.edits) {
|
|
72
|
-
if (originalPos < edit.origStart)
|
|
73
|
-
return undefined;
|
|
74
|
-
if (originalPos < edit.origEnd)
|
|
75
|
-
return edit;
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
exports.SourceMap = SourceMap;
|
|
3
|
+
exports.computeEdits = computeEdits;
|
|
81
4
|
/**
|
|
82
5
|
* Compute edit records by scanning both texts for mismatches.
|
|
6
|
+
* @public
|
|
83
7
|
*
|
|
84
8
|
* Uses character-level comparison with anchor-based convergence:
|
|
85
9
|
* identical characters are skipped, mismatches start an edit region,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditRecord } from "./SourceMap";
|
|
2
|
+
/**
|
|
3
|
+
* Standard V3 source map object.
|
|
4
|
+
* @see https://sourcemaps.info/spec.html
|
|
5
|
+
*/
|
|
6
|
+
export interface V3SourceMap {
|
|
7
|
+
version: 3;
|
|
8
|
+
sources: string[];
|
|
9
|
+
names: string[];
|
|
10
|
+
sourceRoot?: string;
|
|
11
|
+
sourcesContent?: string[];
|
|
12
|
+
mappings: string;
|
|
13
|
+
file: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Convert boperators EditRecord[] into a V3 source map.
|
|
17
|
+
*
|
|
18
|
+
* Generates line-level mappings: unchanged regions map 1:1, and edit
|
|
19
|
+
* boundaries map back to the start of the original edit region.
|
|
20
|
+
*/
|
|
21
|
+
export declare function toV3SourceMap(edits: readonly EditRecord[], originalText: string, transformedText: string, fileName: string): V3SourceMap;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toV3SourceMap = toV3SourceMap;
|
|
4
|
+
/**
|
|
5
|
+
* Convert boperators EditRecord[] into a V3 source map.
|
|
6
|
+
*
|
|
7
|
+
* Generates line-level mappings: unchanged regions map 1:1, and edit
|
|
8
|
+
* boundaries map back to the start of the original edit region.
|
|
9
|
+
*/
|
|
10
|
+
function toV3SourceMap(edits, originalText, transformedText, fileName) {
|
|
11
|
+
const origLineStarts = buildLineStarts(originalText);
|
|
12
|
+
const transLineStarts = buildLineStarts(transformedText);
|
|
13
|
+
// Each segment is [transCol, sourceIndex, origLine, origCol]
|
|
14
|
+
// We only have one source (index 0), so sourceIndex is always 0.
|
|
15
|
+
const mappingLines = [];
|
|
16
|
+
const transLineCount = transLineStarts.length;
|
|
17
|
+
// Initialise empty lines
|
|
18
|
+
for (let i = 0; i < transLineCount; i++) {
|
|
19
|
+
mappingLines.push([]);
|
|
20
|
+
}
|
|
21
|
+
// For positions outside any edit, the mapping is identity + accumulated delta.
|
|
22
|
+
// For positions inside an edit's transformed region, map to the original edit start.
|
|
23
|
+
// We generate one segment per transformed line start.
|
|
24
|
+
for (let transLine = 0; transLine < transLineCount; transLine++) {
|
|
25
|
+
const transOffset = transLineStarts[transLine];
|
|
26
|
+
const origOffset = transformedToOriginal(edits, transOffset);
|
|
27
|
+
const { line: origLine, col: origCol } = offsetToLineCol(origLineStarts, origOffset);
|
|
28
|
+
mappingLines[transLine].push([0, 0, origLine, origCol]);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
version: 3,
|
|
32
|
+
file: fileName,
|
|
33
|
+
sources: [fileName],
|
|
34
|
+
sourcesContent: [originalText],
|
|
35
|
+
names: [],
|
|
36
|
+
mappings: encodeMappings(mappingLines),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Map a transformed-source offset back to the original source. */
|
|
40
|
+
function transformedToOriginal(edits, pos) {
|
|
41
|
+
let delta = 0;
|
|
42
|
+
for (const edit of edits) {
|
|
43
|
+
if (pos < edit.transStart) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (pos < edit.transEnd) {
|
|
47
|
+
return edit.origStart;
|
|
48
|
+
}
|
|
49
|
+
delta = edit.origEnd - edit.transEnd;
|
|
50
|
+
}
|
|
51
|
+
return pos + delta;
|
|
52
|
+
}
|
|
53
|
+
/** Build an array where index i is the character offset of line i. */
|
|
54
|
+
function buildLineStarts(text) {
|
|
55
|
+
const starts = [0];
|
|
56
|
+
for (let i = 0; i < text.length; i++) {
|
|
57
|
+
if (text[i] === "\n") {
|
|
58
|
+
starts.push(i + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return starts;
|
|
62
|
+
}
|
|
63
|
+
/** Convert a character offset to 0-based line and column. */
|
|
64
|
+
function offsetToLineCol(lineStarts, offset) {
|
|
65
|
+
// Binary search for the line containing this offset
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = lineStarts.length - 1;
|
|
68
|
+
while (lo < hi) {
|
|
69
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
70
|
+
if (lineStarts[mid] <= offset) {
|
|
71
|
+
lo = mid;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
hi = mid - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { line: lo, col: offset - lineStarts[lo] };
|
|
78
|
+
}
|
|
79
|
+
/** Encode segment arrays into a VLQ mappings string. */
|
|
80
|
+
function encodeMappings(lines) {
|
|
81
|
+
let prevTransCol = 0;
|
|
82
|
+
let prevOrigLine = 0;
|
|
83
|
+
let prevOrigCol = 0;
|
|
84
|
+
let prevSourceIdx = 0;
|
|
85
|
+
const lineStrings = [];
|
|
86
|
+
for (const segments of lines) {
|
|
87
|
+
prevTransCol = 0; // reset column tracking per line
|
|
88
|
+
const segStrings = [];
|
|
89
|
+
for (const seg of segments) {
|
|
90
|
+
const [transCol, sourceIdx, origLine, origCol] = seg;
|
|
91
|
+
segStrings.push(encodeVLQ(transCol - prevTransCol) +
|
|
92
|
+
encodeVLQ(sourceIdx - prevSourceIdx) +
|
|
93
|
+
encodeVLQ(origLine - prevOrigLine) +
|
|
94
|
+
encodeVLQ(origCol - prevOrigCol));
|
|
95
|
+
prevTransCol = transCol;
|
|
96
|
+
prevSourceIdx = sourceIdx;
|
|
97
|
+
prevOrigLine = origLine;
|
|
98
|
+
prevOrigCol = origCol;
|
|
99
|
+
}
|
|
100
|
+
lineStrings.push(segStrings.join(","));
|
|
101
|
+
}
|
|
102
|
+
return lineStrings.join(";");
|
|
103
|
+
}
|
|
104
|
+
const VLQ_BASE = 32;
|
|
105
|
+
const VLQ_BASE_MASK = VLQ_BASE - 1;
|
|
106
|
+
const VLQ_CONTINUATION = VLQ_BASE;
|
|
107
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
108
|
+
/** Encode a single integer as a VLQ base64 string. */
|
|
109
|
+
function encodeVLQ(value) {
|
|
110
|
+
let vlq = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
111
|
+
let result = "";
|
|
112
|
+
do {
|
|
113
|
+
let digit = vlq & VLQ_BASE_MASK;
|
|
114
|
+
vlq >>>= 5;
|
|
115
|
+
if (vlq > 0) {
|
|
116
|
+
digit |= VLQ_CONTINUATION;
|
|
117
|
+
}
|
|
118
|
+
result += BASE64_CHARS[digit];
|
|
119
|
+
} while (vlq > 0);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
@@ -11,7 +11,9 @@ export { OverloadStore } from "./core/OverloadStore";
|
|
|
11
11
|
export type { OperatorSyntaxKind, PostfixUnaryOperatorSyntaxKind, PrefixUnaryOperatorSyntaxKind, } from "./core/operatorMap";
|
|
12
12
|
export { isOperatorSyntaxKind, isPostfixUnaryOperatorSyntaxKind, isPrefixUnaryOperatorSyntaxKind, } from "./core/operatorMap";
|
|
13
13
|
export type { EditRecord } from "./core/SourceMap";
|
|
14
|
-
export {
|
|
14
|
+
export { computeEdits } from "./core/SourceMap";
|
|
15
|
+
export type { V3SourceMap } from "./core/v3SourceMap";
|
|
16
|
+
export { toV3SourceMap } from "./core/v3SourceMap";
|
|
15
17
|
export type { ExportViolation, ExportViolationReason, ValidateExportsResult, } from "./core/validateExports";
|
|
16
18
|
export { validateExports } from "./core/validateExports";
|
|
17
19
|
export { Operator, operatorSymbols } from "./lib/operatorSymbols";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Core transformation pipeline
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.SyntaxKind = exports.Project = exports.Node = exports.operatorSymbols = exports.Operator = exports.validateExports = exports.
|
|
4
|
+
exports.SyntaxKind = exports.Project = exports.Node = exports.operatorSymbols = exports.Operator = exports.validateExports = exports.toV3SourceMap = exports.computeEdits = exports.isPrefixUnaryOperatorSyntaxKind = exports.isPostfixUnaryOperatorSyntaxKind = exports.isOperatorSyntaxKind = exports.OverloadStore = exports.OverloadInjector = exports.unwrapInitializer = exports.resolveExpressionType = exports.getOperatorStringFromProperty = exports.ErrorManager = exports.ErrorDescription = exports.loadConfig = exports.ConsoleLogger = void 0;
|
|
5
5
|
var BopConfig_1 = require("./core/BopConfig");
|
|
6
6
|
Object.defineProperty(exports, "ConsoleLogger", { enumerable: true, get: function () { return BopConfig_1.ConsoleLogger; } });
|
|
7
7
|
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return BopConfig_1.loadConfig; } });
|
|
@@ -23,7 +23,9 @@ Object.defineProperty(exports, "isOperatorSyntaxKind", { enumerable: true, get:
|
|
|
23
23
|
Object.defineProperty(exports, "isPostfixUnaryOperatorSyntaxKind", { enumerable: true, get: function () { return operatorMap_1.isPostfixUnaryOperatorSyntaxKind; } });
|
|
24
24
|
Object.defineProperty(exports, "isPrefixUnaryOperatorSyntaxKind", { enumerable: true, get: function () { return operatorMap_1.isPrefixUnaryOperatorSyntaxKind; } });
|
|
25
25
|
var SourceMap_1 = require("./core/SourceMap");
|
|
26
|
-
Object.defineProperty(exports, "
|
|
26
|
+
Object.defineProperty(exports, "computeEdits", { enumerable: true, get: function () { return SourceMap_1.computeEdits; } });
|
|
27
|
+
var v3SourceMap_1 = require("./core/v3SourceMap");
|
|
28
|
+
Object.defineProperty(exports, "toV3SourceMap", { enumerable: true, get: function () { return v3SourceMap_1.toV3SourceMap; } });
|
|
27
29
|
var validateExports_1 = require("./core/validateExports");
|
|
28
30
|
Object.defineProperty(exports, "validateExports", { enumerable: true, get: function () { return validateExports_1.validateExports; } });
|
|
29
31
|
// Operator definitions
|
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boperators",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Operator overloading for TypeScript.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/DiefBell/boperators",
|
|
8
|
+
"url": "git+https://github.com/DiefBell/boperators.git",
|
|
9
9
|
"directory": "package"
|
|
10
10
|
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/DiefBell/boperators/issues"
|
|
13
|
+
},
|
|
11
14
|
"homepage": "https://github.com/DiefBell/boperators/tree/main/package",
|
|
12
|
-
"main": "./dist/
|
|
13
|
-
"
|
|
14
|
-
"types": "./dist/esm/index.d.ts",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
15
17
|
"exports": {
|
|
16
18
|
".": {
|
|
17
|
-
"types": "./dist/
|
|
18
|
-
"
|
|
19
|
-
"import": "./dist/esm/index.js"
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|
|
@@ -30,8 +31,10 @@
|
|
|
30
31
|
"transform"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
|
-
"build": "tsc
|
|
34
|
-
"watch": "
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"watch": "tsc -w",
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"test:watch": "bun test --watch",
|
|
35
38
|
"prepare": "bun run build",
|
|
36
39
|
"prepublish": "bun run build"
|
|
37
40
|
},
|
|
@@ -49,7 +52,6 @@
|
|
|
49
52
|
"logo.png"
|
|
50
53
|
],
|
|
51
54
|
"devDependencies": {
|
|
52
|
-
"@types/node": "^22.0.0"
|
|
53
|
-
"concurrently": "^9.2.1"
|
|
55
|
+
"@types/node": "^22.0.0"
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.OverloadInjector = void 0;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
-
const ts_morph_1 = require("ts-morph");
|
|
9
|
-
const ensureImportedName_1 = require("./helpers/ensureImportedName");
|
|
10
|
-
const getModuleSpecifier_1 = require("./helpers/getModuleSpecifier");
|
|
11
|
-
const resolveExpressionType_1 = require("./helpers/resolveExpressionType");
|
|
12
|
-
const operatorMap_1 = require("./operatorMap");
|
|
13
|
-
const SourceMap_1 = require("./SourceMap");
|
|
14
|
-
class OverloadInjector {
|
|
15
|
-
constructor(
|
|
16
|
-
/**
|
|
17
|
-
* TS Morph project.
|
|
18
|
-
*/
|
|
19
|
-
_project,
|
|
20
|
-
/**
|
|
21
|
-
* Overload store.
|
|
22
|
-
*/
|
|
23
|
-
_overloadStore, logger) {
|
|
24
|
-
this._project = _project;
|
|
25
|
-
this._overloadStore = _overloadStore;
|
|
26
|
-
this._logger = logger;
|
|
27
|
-
}
|
|
28
|
-
overloadFile(file) {
|
|
29
|
-
const sourceFile = file instanceof ts_morph_1.SourceFile
|
|
30
|
-
? file
|
|
31
|
-
: this._project.getSourceFileOrThrow(file);
|
|
32
|
-
const fileName = node_path_1.default.basename(sourceFile.getFilePath());
|
|
33
|
-
const originalText = sourceFile.getFullText();
|
|
34
|
-
let transformCount = 0;
|
|
35
|
-
// Process one innermost binary expression per iteration,
|
|
36
|
-
// re-fetching descendants each time so types resolve correctly
|
|
37
|
-
// after each transformation and AST references stay fresh.
|
|
38
|
-
let changed = true;
|
|
39
|
-
while (changed) {
|
|
40
|
-
changed = false;
|
|
41
|
-
// Reverse DFS pre-order → innermost expressions first
|
|
42
|
-
const binaryExpressions = sourceFile
|
|
43
|
-
.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)
|
|
44
|
-
.reverse();
|
|
45
|
-
for (const expression of binaryExpressions) {
|
|
46
|
-
const operatorKind = expression.getOperatorToken().getKind();
|
|
47
|
-
if (!(0, operatorMap_1.isOperatorSyntaxKind)(operatorKind))
|
|
48
|
-
continue;
|
|
49
|
-
const lhs = expression.getLeft();
|
|
50
|
-
const leftType = (0, resolveExpressionType_1.resolveExpressionType)(lhs);
|
|
51
|
-
const rhs = expression.getRight();
|
|
52
|
-
const rightType = (0, resolveExpressionType_1.resolveExpressionType)(rhs);
|
|
53
|
-
const overloadDesc = this._overloadStore.findOverload(operatorKind, leftType, rightType);
|
|
54
|
-
if (!overloadDesc)
|
|
55
|
-
continue;
|
|
56
|
-
const { className: classNameRaw, classFilePath, operatorString, index, isStatic, } = overloadDesc;
|
|
57
|
-
// Look up the fresh ClassDeclaration from the project
|
|
58
|
-
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
59
|
-
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
60
|
-
// Ensure class is imported, get its textual name
|
|
61
|
-
const classModuleSpecifier = (0, getModuleSpecifier_1.getModuleSpecifier)(sourceFile, classSourceFile);
|
|
62
|
-
const className = (0, ensureImportedName_1.ensureImportedName)(sourceFile, classDecl.getSymbol(), classModuleSpecifier);
|
|
63
|
-
// Build the text code to replace the binary operator with the overload call
|
|
64
|
-
const overloadCall = isStatic
|
|
65
|
-
? `${className}["${operatorString}"][${index}](${lhs.getText()}, ${rhs.getText()})`
|
|
66
|
-
: `${lhs.getText()}["${operatorString}"][${index}].call(${lhs.getText()}, ${rhs.getText()})`;
|
|
67
|
-
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
68
|
-
expression.replaceWithText(overloadCall);
|
|
69
|
-
transformCount++;
|
|
70
|
-
changed = true;
|
|
71
|
-
break; // re-fetch descendants after each mutation
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Process prefix unary expressions (-x, +x, !x, ~x)
|
|
75
|
-
changed = true;
|
|
76
|
-
while (changed) {
|
|
77
|
-
changed = false;
|
|
78
|
-
const prefixExpressions = sourceFile
|
|
79
|
-
.getDescendantsOfKind(ts_morph_1.SyntaxKind.PrefixUnaryExpression)
|
|
80
|
-
.reverse();
|
|
81
|
-
for (const expression of prefixExpressions) {
|
|
82
|
-
const operatorKind = expression.getOperatorToken();
|
|
83
|
-
if (!(0, operatorMap_1.isPrefixUnaryOperatorSyntaxKind)(operatorKind))
|
|
84
|
-
continue;
|
|
85
|
-
const operand = expression.getOperand();
|
|
86
|
-
const operandType = (0, resolveExpressionType_1.resolveExpressionType)(operand);
|
|
87
|
-
const overloadDesc = this._overloadStore.findPrefixUnaryOverload(operatorKind, operandType);
|
|
88
|
-
if (!overloadDesc)
|
|
89
|
-
continue;
|
|
90
|
-
const { className: classNameRaw, classFilePath, operatorString, index, } = overloadDesc;
|
|
91
|
-
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
92
|
-
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
93
|
-
const classModuleSpecifier = (0, getModuleSpecifier_1.getModuleSpecifier)(sourceFile, classSourceFile);
|
|
94
|
-
const className = (0, ensureImportedName_1.ensureImportedName)(sourceFile, classDecl.getSymbol(), classModuleSpecifier);
|
|
95
|
-
const overloadCall = `${className}["${operatorString}"][${index}](${operand.getText()})`;
|
|
96
|
-
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
97
|
-
expression.replaceWithText(overloadCall);
|
|
98
|
-
transformCount++;
|
|
99
|
-
changed = true;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Process postfix unary expressions (x++, x--)
|
|
104
|
-
changed = true;
|
|
105
|
-
while (changed) {
|
|
106
|
-
changed = false;
|
|
107
|
-
const postfixExpressions = sourceFile
|
|
108
|
-
.getDescendantsOfKind(ts_morph_1.SyntaxKind.PostfixUnaryExpression)
|
|
109
|
-
.reverse();
|
|
110
|
-
for (const expression of postfixExpressions) {
|
|
111
|
-
const operatorKind = expression.getOperatorToken();
|
|
112
|
-
if (!(0, operatorMap_1.isPostfixUnaryOperatorSyntaxKind)(operatorKind))
|
|
113
|
-
continue;
|
|
114
|
-
const operand = expression.getOperand();
|
|
115
|
-
const operandType = (0, resolveExpressionType_1.resolveExpressionType)(operand);
|
|
116
|
-
const overloadDesc = this._overloadStore.findPostfixUnaryOverload(operatorKind, operandType);
|
|
117
|
-
if (!overloadDesc)
|
|
118
|
-
continue;
|
|
119
|
-
const { operatorString, index } = overloadDesc;
|
|
120
|
-
const overloadCall = `${operand.getText()}["${operatorString}"][${index}].call(${operand.getText()})`;
|
|
121
|
-
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
122
|
-
expression.replaceWithText(overloadCall);
|
|
123
|
-
transformCount++;
|
|
124
|
-
changed = true;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (transformCount > 0) {
|
|
129
|
-
this._logger.debug(`${fileName}: ${transformCount} expression${transformCount === 1 ? "" : "s"} transformed`);
|
|
130
|
-
}
|
|
131
|
-
const text = sourceFile.getFullText();
|
|
132
|
-
const sourceMap = new SourceMap_1.SourceMap(originalText, text);
|
|
133
|
-
return { sourceFile, text, sourceMap };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
exports.OverloadInjector = OverloadInjector;
|