boperators 0.1.0
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 +202 -0
- package/dist/cjs/core/BopConfig.js +73 -0
- package/dist/cjs/core/ErrorManager.js +126 -0
- package/dist/cjs/core/OverloadInjector.js +136 -0
- package/dist/cjs/core/OverloadStore.js +622 -0
- package/dist/cjs/core/SourceMap.js +168 -0
- package/dist/cjs/core/helpers/ensureImportedName.js +50 -0
- package/dist/cjs/core/helpers/getImportedNameForSymbol.js +64 -0
- package/dist/cjs/core/helpers/getModuleSpecifier.js +61 -0
- package/dist/cjs/core/helpers/getOperatorStringFromProperty.js +33 -0
- package/dist/cjs/core/helpers/resolveExpressionType.js +30 -0
- package/dist/cjs/core/helpers/unwrapInitializer.js +18 -0
- package/dist/cjs/core/operatorMap.js +95 -0
- package/dist/cjs/core/validateExports.js +87 -0
- package/dist/cjs/index.js +36 -0
- package/dist/cjs/lib/index.js +17 -0
- package/dist/cjs/lib/operatorSymbols.js +37 -0
- package/dist/esm/core/BopConfig.d.ts +34 -0
- package/dist/esm/core/BopConfig.js +65 -0
- package/dist/esm/core/ErrorManager.d.ts +90 -0
- package/dist/esm/core/ErrorManager.js +118 -0
- package/dist/esm/core/OverloadInjector.d.ts +33 -0
- package/dist/esm/core/OverloadInjector.js +129 -0
- package/dist/esm/core/OverloadStore.d.ts +114 -0
- package/dist/esm/core/OverloadStore.js +618 -0
- package/dist/esm/core/SourceMap.d.ts +41 -0
- package/dist/esm/core/SourceMap.js +164 -0
- package/dist/esm/core/helpers/ensureImportedName.d.ts +11 -0
- package/dist/esm/core/helpers/ensureImportedName.js +46 -0
- package/dist/esm/core/helpers/getImportedNameForSymbol.d.ts +8 -0
- package/dist/esm/core/helpers/getImportedNameForSymbol.js +60 -0
- package/dist/esm/core/helpers/getModuleSpecifier.d.ts +2 -0
- package/dist/esm/core/helpers/getModuleSpecifier.js +57 -0
- package/dist/esm/core/helpers/getOperatorStringFromProperty.d.ts +13 -0
- package/dist/esm/core/helpers/getOperatorStringFromProperty.js +30 -0
- package/dist/esm/core/helpers/resolveExpressionType.d.ts +11 -0
- package/dist/esm/core/helpers/resolveExpressionType.js +27 -0
- package/dist/esm/core/helpers/unwrapInitializer.d.ts +9 -0
- package/dist/esm/core/helpers/unwrapInitializer.js +15 -0
- package/dist/esm/core/operatorMap.d.ts +77 -0
- package/dist/esm/core/operatorMap.js +89 -0
- package/dist/esm/core/validateExports.d.ts +30 -0
- package/dist/esm/core/validateExports.js +84 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/lib/index.d.ts +1 -0
- package/dist/esm/lib/index.js +1 -0
- package/dist/esm/lib/operatorSymbols.d.ts +33 -0
- package/dist/esm/lib/operatorSymbols.js +34 -0
- package/license.txt +8 -0
- package/package.json +54 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.operatorSymbols = exports.Operator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Designed for internal use to ensure consistency.
|
|
6
|
+
*/
|
|
7
|
+
var Operator;
|
|
8
|
+
(function (Operator) {
|
|
9
|
+
Operator["PLUS"] = "+";
|
|
10
|
+
Operator["PLUS_EQUALS"] = "+=";
|
|
11
|
+
Operator["MINUS"] = "-";
|
|
12
|
+
Operator["MINUS_EQUALS"] = "-=";
|
|
13
|
+
Operator["MULTIPLY"] = "*";
|
|
14
|
+
Operator["MULTIPLY_EQUALS"] = "*=";
|
|
15
|
+
Operator["DIVIDE"] = "/";
|
|
16
|
+
Operator["DIVIDE_EQUALS"] = "/=";
|
|
17
|
+
Operator["GREATER_THAN"] = ">";
|
|
18
|
+
Operator["GREATER_THAN_EQUAL_TO"] = ">=";
|
|
19
|
+
Operator["LESS_THAN"] = "<";
|
|
20
|
+
Operator["LESS_THAN_EQUAL_TO"] = "<=";
|
|
21
|
+
Operator["MODULO"] = "%";
|
|
22
|
+
Operator["MODULO_EQUALS"] = "%=";
|
|
23
|
+
Operator["EQUALS"] = "==";
|
|
24
|
+
Operator["STRICT_EQUALS"] = "===";
|
|
25
|
+
Operator["NOT_EQUALS"] = "!=";
|
|
26
|
+
Operator["STRICT_NOT_EQUALS"] = "!==";
|
|
27
|
+
Operator["AND"] = "&&";
|
|
28
|
+
Operator["AND_EQUALS"] = "&&=";
|
|
29
|
+
Operator["OR"] = "||";
|
|
30
|
+
Operator["OR_EQUALS"] = "||=";
|
|
31
|
+
Operator["NULLISH"] = "??";
|
|
32
|
+
Operator["NOT"] = "!";
|
|
33
|
+
Operator["BITWISE_NOT"] = "~";
|
|
34
|
+
Operator["INCREMENT"] = "++";
|
|
35
|
+
Operator["DECREMENT"] = "--";
|
|
36
|
+
})(Operator || (exports.Operator = Operator = {}));
|
|
37
|
+
exports.operatorSymbols = Object.values(Operator);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
2
|
+
export interface BopLogger {
|
|
3
|
+
debug(message: string): void;
|
|
4
|
+
info(message: string): void;
|
|
5
|
+
warn(message: string): void;
|
|
6
|
+
error(message: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface BopConfig {
|
|
9
|
+
errorOnWarning: boolean;
|
|
10
|
+
logLevel: LogLevel;
|
|
11
|
+
logger: BopLogger;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serialisable subset of {@link BopConfig} — the shape of `.bopconf.json`.
|
|
15
|
+
*/
|
|
16
|
+
export interface BopConfFile {
|
|
17
|
+
errorOnWarning?: boolean;
|
|
18
|
+
logLevel?: LogLevel;
|
|
19
|
+
}
|
|
20
|
+
export declare class ConsoleLogger implements BopLogger {
|
|
21
|
+
debug(msg: string): void;
|
|
22
|
+
info(msg: string): void;
|
|
23
|
+
warn(msg: string): void;
|
|
24
|
+
error(msg: string): void;
|
|
25
|
+
}
|
|
26
|
+
export interface LoadConfigOptions {
|
|
27
|
+
/** Directory to start searching for `.bopconf.json` (default: `process.cwd()`). */
|
|
28
|
+
searchDir?: string;
|
|
29
|
+
/** Programmatic overrides applied on top of file config (e.g. CLI flags, plugin config). */
|
|
30
|
+
overrides?: Partial<BopConfFile>;
|
|
31
|
+
/** Custom logger implementation (e.g. TS language server logger). */
|
|
32
|
+
logger?: BopLogger;
|
|
33
|
+
}
|
|
34
|
+
export declare function loadConfig(options?: LoadConfigOptions): BopConfig;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CONFIG_FILE_NAME = ".bopconf.json";
|
|
4
|
+
// ----- ConsoleLogger -----
|
|
5
|
+
export class ConsoleLogger {
|
|
6
|
+
debug(msg) {
|
|
7
|
+
console.debug(`[boperators] ${msg}`);
|
|
8
|
+
}
|
|
9
|
+
info(msg) {
|
|
10
|
+
console.log(`[boperators] ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
warn(msg) {
|
|
13
|
+
console.warn(`[boperators] ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
error(msg) {
|
|
16
|
+
console.error(`[boperators] ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// ----- Filtered logger -----
|
|
20
|
+
const LOG_LEVELS = ["debug", "info", "warn", "error", "silent"];
|
|
21
|
+
function createFilteredLogger(inner, level) {
|
|
22
|
+
const minIndex = LOG_LEVELS.indexOf(level);
|
|
23
|
+
const noop = () => { };
|
|
24
|
+
return {
|
|
25
|
+
debug: minIndex <= 0 ? inner.debug.bind(inner) : noop,
|
|
26
|
+
info: minIndex <= 1 ? inner.info.bind(inner) : noop,
|
|
27
|
+
warn: minIndex <= 2 ? inner.warn.bind(inner) : noop,
|
|
28
|
+
error: minIndex <= 3 ? inner.error.bind(inner) : noop,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// ----- Config file discovery -----
|
|
32
|
+
function findConfigFile(startDir) {
|
|
33
|
+
let dir = path.resolve(startDir);
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = path.join(dir, CONFIG_FILE_NAME);
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(candidate, "utf-8");
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch (_a) {
|
|
41
|
+
// File not found or invalid JSON — keep walking up
|
|
42
|
+
}
|
|
43
|
+
const parent = path.dirname(dir);
|
|
44
|
+
if (parent === dir)
|
|
45
|
+
break; // filesystem root
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const DEFAULTS = {
|
|
51
|
+
errorOnWarning: false,
|
|
52
|
+
logLevel: "info",
|
|
53
|
+
};
|
|
54
|
+
export function loadConfig(options) {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
const fileConfig = findConfigFile((_a = options === null || options === void 0 ? void 0 : options.searchDir) !== null && _a !== void 0 ? _a : process.cwd());
|
|
57
|
+
const merged = Object.assign(Object.assign(Object.assign({}, DEFAULTS), fileConfig), options === null || options === void 0 ? void 0 : options.overrides);
|
|
58
|
+
const baseLogger = (_b = options === null || options === void 0 ? void 0 : options.logger) !== null && _b !== void 0 ? _b : new ConsoleLogger();
|
|
59
|
+
const logger = createFilteredLogger(baseLogger, merged.logLevel);
|
|
60
|
+
return {
|
|
61
|
+
errorOnWarning: merged.errorOnWarning,
|
|
62
|
+
logLevel: merged.logLevel,
|
|
63
|
+
logger,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BopConfig } from "./BopConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stores information about an error or warning for later use.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ErrorDescription {
|
|
6
|
+
readonly errorMessage: string;
|
|
7
|
+
readonly filePath: string;
|
|
8
|
+
readonly lineNumber: number;
|
|
9
|
+
readonly codeText: string;
|
|
10
|
+
/**
|
|
11
|
+
* The base filename where the error occurred,
|
|
12
|
+
* so we don't need the entire filepath.
|
|
13
|
+
*/
|
|
14
|
+
readonly fileName: string;
|
|
15
|
+
/**
|
|
16
|
+
* Create an ErrorDescription.
|
|
17
|
+
*
|
|
18
|
+
* @param errorMessage Custom message text describing the error.
|
|
19
|
+
* @param filePath Error file path.
|
|
20
|
+
* @param lineNumber Error line number in its file.
|
|
21
|
+
* @param codeText Code relevant to the error.q
|
|
22
|
+
*/
|
|
23
|
+
constructor(errorMessage: string, filePath: string, lineNumber: number, codeText: string);
|
|
24
|
+
toString(): string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stores errors and warnings in a way that means
|
|
28
|
+
* they can be logged or thrown at a later point.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ErrorManager {
|
|
31
|
+
/**
|
|
32
|
+
* Array of warnings.
|
|
33
|
+
*/
|
|
34
|
+
private _warnings;
|
|
35
|
+
/**
|
|
36
|
+
* Array of errors.
|
|
37
|
+
*/
|
|
38
|
+
private _errors;
|
|
39
|
+
/**
|
|
40
|
+
* Whether or not warnings will cause this to throw.
|
|
41
|
+
*/
|
|
42
|
+
private readonly _errorOnWarning;
|
|
43
|
+
private readonly _config;
|
|
44
|
+
constructor(config: BopConfig);
|
|
45
|
+
/**
|
|
46
|
+
* Add an ErrorDescription or a string as a "warning",
|
|
47
|
+
* meaning it will not throw unless `errorOnWarning` is true.
|
|
48
|
+
* @param description Either a string describing the error or a {@link ErrorDescription} instance.
|
|
49
|
+
*/
|
|
50
|
+
addWarning(description: ErrorDescription | string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Add an ErrorDescription or a string as an "error",
|
|
53
|
+
* meaning it will throw when checked.
|
|
54
|
+
* @param description Either a string describing the error or a {@link ErrorDescription} instance.
|
|
55
|
+
*/
|
|
56
|
+
addError(description: ErrorDescription | string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Gets all warnings as a single string, separated by newlines.
|
|
59
|
+
* @returns String of all warnings.
|
|
60
|
+
*/
|
|
61
|
+
getWarningString(): string;
|
|
62
|
+
/**
|
|
63
|
+
* Gets all errors as a single string, separated by newlines.
|
|
64
|
+
* @returns String of all errors.
|
|
65
|
+
*/
|
|
66
|
+
getErrorsString(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Throws if there are any errors currently registered in the ErrorManager.
|
|
69
|
+
* If `errorOnWarning` is true then it will also throw if there are warnings.
|
|
70
|
+
*/
|
|
71
|
+
throwIfErrors(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Will throw if there are any errors, or if there are warnings and `errorOnWarning` is true.
|
|
74
|
+
* If it does not throw then it will log all wanrnings to the console.
|
|
75
|
+
* @param clearSelf If true, all warnings will be cleared after logging.
|
|
76
|
+
*/
|
|
77
|
+
throwIfErrorsElseLogWarnings(clearSelf?: boolean): void;
|
|
78
|
+
/**
|
|
79
|
+
* Clear out registered warnings to prevent duplicate logging.
|
|
80
|
+
*/
|
|
81
|
+
clearWarnings(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Clear out registered errors.
|
|
84
|
+
*/
|
|
85
|
+
clearErrors(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Clear all registered errors and warnings.
|
|
88
|
+
*/
|
|
89
|
+
clear(): void;
|
|
90
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Stores information about an error or warning for later use.
|
|
4
|
+
*/
|
|
5
|
+
export class ErrorDescription {
|
|
6
|
+
/**
|
|
7
|
+
* Create an ErrorDescription.
|
|
8
|
+
*
|
|
9
|
+
* @param errorMessage Custom message text describing the error.
|
|
10
|
+
* @param filePath Error file path.
|
|
11
|
+
* @param lineNumber Error line number in its file.
|
|
12
|
+
* @param codeText Code relevant to the error.q
|
|
13
|
+
*/
|
|
14
|
+
constructor(errorMessage, filePath, lineNumber, codeText) {
|
|
15
|
+
this.errorMessage = errorMessage;
|
|
16
|
+
this.filePath = filePath;
|
|
17
|
+
this.lineNumber = lineNumber;
|
|
18
|
+
this.codeText = codeText;
|
|
19
|
+
this.fileName = path.basename(filePath);
|
|
20
|
+
}
|
|
21
|
+
toString() {
|
|
22
|
+
return `${this.fileName}:${this.lineNumber}: ${this.errorMessage}\n${this.codeText}\n`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Stores errors and warnings in a way that means
|
|
27
|
+
* they can be logged or thrown at a later point.
|
|
28
|
+
*/
|
|
29
|
+
export class ErrorManager {
|
|
30
|
+
constructor(config) {
|
|
31
|
+
/**
|
|
32
|
+
* Array of warnings.
|
|
33
|
+
*/
|
|
34
|
+
this._warnings = [];
|
|
35
|
+
/**
|
|
36
|
+
* Array of errors.
|
|
37
|
+
*/
|
|
38
|
+
this._errors = [];
|
|
39
|
+
this._config = config;
|
|
40
|
+
this._errorOnWarning = config.errorOnWarning;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Add an ErrorDescription or a string as a "warning",
|
|
44
|
+
* meaning it will not throw unless `errorOnWarning` is true.
|
|
45
|
+
* @param description Either a string describing the error or a {@link ErrorDescription} instance.
|
|
46
|
+
*/
|
|
47
|
+
addWarning(description) {
|
|
48
|
+
this._warnings.push(description);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Add an ErrorDescription or a string as an "error",
|
|
52
|
+
* meaning it will throw when checked.
|
|
53
|
+
* @param description Either a string describing the error or a {@link ErrorDescription} instance.
|
|
54
|
+
*/
|
|
55
|
+
addError(description) {
|
|
56
|
+
this._errors.push(description);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Gets all warnings as a single string, separated by newlines.
|
|
60
|
+
* @returns String of all warnings.
|
|
61
|
+
*/
|
|
62
|
+
getWarningString() {
|
|
63
|
+
return this._warnings.map((warning) => warning.toString()).join("\n");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Gets all errors as a single string, separated by newlines.
|
|
67
|
+
* @returns String of all errors.
|
|
68
|
+
*/
|
|
69
|
+
getErrorsString() {
|
|
70
|
+
return this._errors.map((error) => error.toString()).join("\n");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Throws if there are any errors currently registered in the ErrorManager.
|
|
74
|
+
* If `errorOnWarning` is true then it will also throw if there are warnings.
|
|
75
|
+
*/
|
|
76
|
+
throwIfErrors() {
|
|
77
|
+
const shouldThrow = this._errors.length > 0 ||
|
|
78
|
+
(this._errorOnWarning && this._warnings.length > 0);
|
|
79
|
+
if (!shouldThrow)
|
|
80
|
+
return;
|
|
81
|
+
const errorString = this.getErrorsString() +
|
|
82
|
+
(this._errorOnWarning ? this.getWarningString() : "");
|
|
83
|
+
throw new Error(errorString);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Will throw if there are any errors, or if there are warnings and `errorOnWarning` is true.
|
|
87
|
+
* If it does not throw then it will log all wanrnings to the console.
|
|
88
|
+
* @param clearSelf If true, all warnings will be cleared after logging.
|
|
89
|
+
*/
|
|
90
|
+
throwIfErrorsElseLogWarnings(clearSelf = true) {
|
|
91
|
+
this.throwIfErrors();
|
|
92
|
+
if (this._warnings.length > 0) {
|
|
93
|
+
this._config.logger.warn(this.getWarningString());
|
|
94
|
+
}
|
|
95
|
+
if (clearSelf) {
|
|
96
|
+
this.clearWarnings();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clear out registered warnings to prevent duplicate logging.
|
|
101
|
+
*/
|
|
102
|
+
clearWarnings() {
|
|
103
|
+
this._warnings = [];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Clear out registered errors.
|
|
107
|
+
*/
|
|
108
|
+
clearErrors() {
|
|
109
|
+
this._errors = [];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Clear all registered errors and warnings.
|
|
113
|
+
*/
|
|
114
|
+
clear() {
|
|
115
|
+
this._errors = [];
|
|
116
|
+
this._warnings = [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SourceFile, type Project as TsMorphProject } from "ts-morph";
|
|
2
|
+
import type { BopLogger } from "./BopConfig";
|
|
3
|
+
import type { OverloadStore } from "./OverloadStore";
|
|
4
|
+
import { SourceMap } from "./SourceMap";
|
|
5
|
+
export type TransformResult = {
|
|
6
|
+
/** The mutated ts-morph SourceFile (same reference as input). */
|
|
7
|
+
sourceFile: SourceFile;
|
|
8
|
+
/** The full text after transformation. */
|
|
9
|
+
text: string;
|
|
10
|
+
/** Bidirectional source map between original and transformed text. */
|
|
11
|
+
sourceMap: SourceMap;
|
|
12
|
+
};
|
|
13
|
+
export declare class OverloadInjector {
|
|
14
|
+
/**
|
|
15
|
+
* TS Morph project.
|
|
16
|
+
*/
|
|
17
|
+
private readonly _project;
|
|
18
|
+
/**
|
|
19
|
+
* Overload store.
|
|
20
|
+
*/
|
|
21
|
+
private readonly _overloadStore;
|
|
22
|
+
private readonly _logger;
|
|
23
|
+
constructor(
|
|
24
|
+
/**
|
|
25
|
+
* TS Morph project.
|
|
26
|
+
*/
|
|
27
|
+
_project: TsMorphProject,
|
|
28
|
+
/**
|
|
29
|
+
* Overload store.
|
|
30
|
+
*/
|
|
31
|
+
_overloadStore: OverloadStore, logger: BopLogger);
|
|
32
|
+
overloadFile(file: string | SourceFile): TransformResult;
|
|
33
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SourceFile, SyntaxKind, } from "ts-morph";
|
|
3
|
+
import { ensureImportedName } from "./helpers/ensureImportedName";
|
|
4
|
+
import { getModuleSpecifier } from "./helpers/getModuleSpecifier";
|
|
5
|
+
import { resolveExpressionType } from "./helpers/resolveExpressionType";
|
|
6
|
+
import { isOperatorSyntaxKind, isPostfixUnaryOperatorSyntaxKind, isPrefixUnaryOperatorSyntaxKind, } from "./operatorMap";
|
|
7
|
+
import { SourceMap } from "./SourceMap";
|
|
8
|
+
export class OverloadInjector {
|
|
9
|
+
constructor(
|
|
10
|
+
/**
|
|
11
|
+
* TS Morph project.
|
|
12
|
+
*/
|
|
13
|
+
_project,
|
|
14
|
+
/**
|
|
15
|
+
* Overload store.
|
|
16
|
+
*/
|
|
17
|
+
_overloadStore, logger) {
|
|
18
|
+
this._project = _project;
|
|
19
|
+
this._overloadStore = _overloadStore;
|
|
20
|
+
this._logger = logger;
|
|
21
|
+
}
|
|
22
|
+
overloadFile(file) {
|
|
23
|
+
const sourceFile = file instanceof SourceFile
|
|
24
|
+
? file
|
|
25
|
+
: this._project.getSourceFileOrThrow(file);
|
|
26
|
+
const fileName = path.basename(sourceFile.getFilePath());
|
|
27
|
+
const originalText = sourceFile.getFullText();
|
|
28
|
+
let transformCount = 0;
|
|
29
|
+
// Process one innermost binary expression per iteration,
|
|
30
|
+
// re-fetching descendants each time so types resolve correctly
|
|
31
|
+
// after each transformation and AST references stay fresh.
|
|
32
|
+
let changed = true;
|
|
33
|
+
while (changed) {
|
|
34
|
+
changed = false;
|
|
35
|
+
// Reverse DFS pre-order → innermost expressions first
|
|
36
|
+
const binaryExpressions = sourceFile
|
|
37
|
+
.getDescendantsOfKind(SyntaxKind.BinaryExpression)
|
|
38
|
+
.reverse();
|
|
39
|
+
for (const expression of binaryExpressions) {
|
|
40
|
+
const operatorKind = expression.getOperatorToken().getKind();
|
|
41
|
+
if (!isOperatorSyntaxKind(operatorKind))
|
|
42
|
+
continue;
|
|
43
|
+
const lhs = expression.getLeft();
|
|
44
|
+
const leftType = resolveExpressionType(lhs);
|
|
45
|
+
const rhs = expression.getRight();
|
|
46
|
+
const rightType = resolveExpressionType(rhs);
|
|
47
|
+
const overloadDesc = this._overloadStore.findOverload(operatorKind, leftType, rightType);
|
|
48
|
+
if (!overloadDesc)
|
|
49
|
+
continue;
|
|
50
|
+
const { className: classNameRaw, classFilePath, operatorString, index, isStatic, } = overloadDesc;
|
|
51
|
+
// Look up the fresh ClassDeclaration from the project
|
|
52
|
+
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
53
|
+
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
54
|
+
// Ensure class is imported, get its textual name
|
|
55
|
+
const classModuleSpecifier = getModuleSpecifier(sourceFile, classSourceFile);
|
|
56
|
+
const className = ensureImportedName(sourceFile, classDecl.getSymbol(), classModuleSpecifier);
|
|
57
|
+
// Build the text code to replace the binary operator with the overload call
|
|
58
|
+
const overloadCall = isStatic
|
|
59
|
+
? `${className}["${operatorString}"][${index}](${lhs.getText()}, ${rhs.getText()})`
|
|
60
|
+
: `${lhs.getText()}["${operatorString}"][${index}].call(${lhs.getText()}, ${rhs.getText()})`;
|
|
61
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
62
|
+
expression.replaceWithText(overloadCall);
|
|
63
|
+
transformCount++;
|
|
64
|
+
changed = true;
|
|
65
|
+
break; // re-fetch descendants after each mutation
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Process prefix unary expressions (-x, +x, !x, ~x)
|
|
69
|
+
changed = true;
|
|
70
|
+
while (changed) {
|
|
71
|
+
changed = false;
|
|
72
|
+
const prefixExpressions = sourceFile
|
|
73
|
+
.getDescendantsOfKind(SyntaxKind.PrefixUnaryExpression)
|
|
74
|
+
.reverse();
|
|
75
|
+
for (const expression of prefixExpressions) {
|
|
76
|
+
const operatorKind = expression.getOperatorToken();
|
|
77
|
+
if (!isPrefixUnaryOperatorSyntaxKind(operatorKind))
|
|
78
|
+
continue;
|
|
79
|
+
const operand = expression.getOperand();
|
|
80
|
+
const operandType = resolveExpressionType(operand);
|
|
81
|
+
const overloadDesc = this._overloadStore.findPrefixUnaryOverload(operatorKind, operandType);
|
|
82
|
+
if (!overloadDesc)
|
|
83
|
+
continue;
|
|
84
|
+
const { className: classNameRaw, classFilePath, operatorString, index, } = overloadDesc;
|
|
85
|
+
const classSourceFile = this._project.getSourceFileOrThrow(classFilePath);
|
|
86
|
+
const classDecl = classSourceFile.getClassOrThrow(classNameRaw);
|
|
87
|
+
const classModuleSpecifier = getModuleSpecifier(sourceFile, classSourceFile);
|
|
88
|
+
const className = ensureImportedName(sourceFile, classDecl.getSymbol(), classModuleSpecifier);
|
|
89
|
+
const overloadCall = `${className}["${operatorString}"][${index}](${operand.getText()})`;
|
|
90
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
91
|
+
expression.replaceWithText(overloadCall);
|
|
92
|
+
transformCount++;
|
|
93
|
+
changed = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Process postfix unary expressions (x++, x--)
|
|
98
|
+
changed = true;
|
|
99
|
+
while (changed) {
|
|
100
|
+
changed = false;
|
|
101
|
+
const postfixExpressions = sourceFile
|
|
102
|
+
.getDescendantsOfKind(SyntaxKind.PostfixUnaryExpression)
|
|
103
|
+
.reverse();
|
|
104
|
+
for (const expression of postfixExpressions) {
|
|
105
|
+
const operatorKind = expression.getOperatorToken();
|
|
106
|
+
if (!isPostfixUnaryOperatorSyntaxKind(operatorKind))
|
|
107
|
+
continue;
|
|
108
|
+
const operand = expression.getOperand();
|
|
109
|
+
const operandType = resolveExpressionType(operand);
|
|
110
|
+
const overloadDesc = this._overloadStore.findPostfixUnaryOverload(operatorKind, operandType);
|
|
111
|
+
if (!overloadDesc)
|
|
112
|
+
continue;
|
|
113
|
+
const { operatorString, index } = overloadDesc;
|
|
114
|
+
const overloadCall = `${operand.getText()}["${operatorString}"][${index}].call(${operand.getText()})`;
|
|
115
|
+
this._logger.debug(`${fileName}: ${expression.getText()} => ${overloadCall}`);
|
|
116
|
+
expression.replaceWithText(overloadCall);
|
|
117
|
+
transformCount++;
|
|
118
|
+
changed = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (transformCount > 0) {
|
|
123
|
+
this._logger.debug(`${fileName}: ${transformCount} expression${transformCount === 1 ? "" : "s"} transformed`);
|
|
124
|
+
}
|
|
125
|
+
const text = sourceFile.getFullText();
|
|
126
|
+
const sourceMap = new SourceMap(originalText, text);
|
|
127
|
+
return { sourceFile, text, sourceMap };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SourceFile, type Project as TsMorphProject } from "ts-morph";
|
|
2
|
+
import type { BopLogger } from "./BopConfig";
|
|
3
|
+
import { type ErrorManager } from "./ErrorManager";
|
|
4
|
+
import { type OperatorSyntaxKind, type PostfixUnaryOperatorSyntaxKind, type PrefixUnaryOperatorSyntaxKind } from "./operatorMap";
|
|
5
|
+
/**
|
|
6
|
+
* Name of the type of node on the left-hand side of the operator.
|
|
7
|
+
* Exists purely to make it clear what's where in the map typings.
|
|
8
|
+
*/
|
|
9
|
+
type LhsTypeName = string;
|
|
10
|
+
/**
|
|
11
|
+
* Name of the type of node on the right-hand side of the operator.
|
|
12
|
+
* Exists purely to make it clear what's where in the map typings.
|
|
13
|
+
*/
|
|
14
|
+
type RhsTypeName = string;
|
|
15
|
+
/**
|
|
16
|
+
* Information about an overload so that we can
|
|
17
|
+
* substitute it over binary expressions.
|
|
18
|
+
*/
|
|
19
|
+
export type OverloadDescription = {
|
|
20
|
+
isStatic: boolean;
|
|
21
|
+
className: string;
|
|
22
|
+
classFilePath: string;
|
|
23
|
+
operatorString: string;
|
|
24
|
+
index: number;
|
|
25
|
+
returnType: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A flat representation of a registered overload, suitable for
|
|
29
|
+
* external consumption (e.g. MCP server, tooling).
|
|
30
|
+
*/
|
|
31
|
+
export type OverloadInfo = {
|
|
32
|
+
kind: "binary" | "prefixUnary" | "postfixUnary";
|
|
33
|
+
className: string;
|
|
34
|
+
classFilePath: string;
|
|
35
|
+
operatorString: string;
|
|
36
|
+
index: number;
|
|
37
|
+
isStatic: boolean;
|
|
38
|
+
/** LHS type name (binary overloads only). */
|
|
39
|
+
lhsType?: string;
|
|
40
|
+
/** RHS type name (binary overloads only). */
|
|
41
|
+
rhsType?: string;
|
|
42
|
+
/** Operand type name (unary overloads only). */
|
|
43
|
+
operandType?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare class OverloadStore extends Map<OperatorSyntaxKind, Map<LhsTypeName, Map<RhsTypeName, OverloadDescription>>> {
|
|
46
|
+
private readonly _project;
|
|
47
|
+
private readonly _errorManager;
|
|
48
|
+
private readonly _parsedFiles;
|
|
49
|
+
/** Tracks which map entries came from which file, for targeted invalidation. */
|
|
50
|
+
private readonly _fileEntries;
|
|
51
|
+
/** Cache for type hierarchy chains: type name → [self, parent, grandparent, ...] */
|
|
52
|
+
private readonly _typeChainCache;
|
|
53
|
+
/** Storage for prefix unary overloads: operator → operandType → description */
|
|
54
|
+
private readonly _prefixUnaryOverloads;
|
|
55
|
+
/** Storage for postfix unary overloads: operator → operandType → description */
|
|
56
|
+
private readonly _postfixUnaryOverloads;
|
|
57
|
+
/** Tracks which prefix unary entries came from which file, for targeted invalidation. */
|
|
58
|
+
private readonly _prefixUnaryFileEntries;
|
|
59
|
+
/** Tracks which postfix unary entries came from which file, for targeted invalidation. */
|
|
60
|
+
private readonly _postfixUnaryFileEntries;
|
|
61
|
+
private readonly _logger;
|
|
62
|
+
constructor(project: TsMorphProject, errorManager: ErrorManager, logger: BopLogger);
|
|
63
|
+
/**
|
|
64
|
+
* Looks up an overload description for the given operator kind and
|
|
65
|
+
* LHS/RHS type pair. Walks up the class hierarchy for both sides
|
|
66
|
+
* when an exact match isn't found.
|
|
67
|
+
*/
|
|
68
|
+
findOverload(operatorKind: OperatorSyntaxKind, lhsType: string, rhsType: string): OverloadDescription | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Looks up a prefix unary overload description for the given operator kind
|
|
71
|
+
* and operand type. Walks up the class hierarchy when an exact match isn't found.
|
|
72
|
+
*/
|
|
73
|
+
findPrefixUnaryOverload(operatorKind: PrefixUnaryOperatorSyntaxKind, operandType: string): OverloadDescription | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Looks up a postfix unary overload description for the given operator kind
|
|
76
|
+
* and operand type. Walks up the class hierarchy when an exact match isn't found.
|
|
77
|
+
*/
|
|
78
|
+
findPostfixUnaryOverload(operatorKind: PostfixUnaryOperatorSyntaxKind, operandType: string): OverloadDescription | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Returns a flat array of all registered overloads (binary, prefix unary,
|
|
81
|
+
* and postfix unary) with clean type names.
|
|
82
|
+
*/
|
|
83
|
+
getAllOverloads(): OverloadInfo[];
|
|
84
|
+
/**
|
|
85
|
+
* Removes all overload entries that originated from the given file
|
|
86
|
+
* and marks it as unparsed so it will be re-scanned on the next call
|
|
87
|
+
* to `addOverloadsFromFile`.
|
|
88
|
+
*
|
|
89
|
+
* @returns `true` if the file had any overload entries (meaning files
|
|
90
|
+
* that were transformed using those overloads may now be stale).
|
|
91
|
+
*/
|
|
92
|
+
invalidateFile(filePath: string): boolean;
|
|
93
|
+
addOverloadsFromFile(file: string | SourceFile): void;
|
|
94
|
+
private _addBinaryOverload;
|
|
95
|
+
private _addPrefixUnaryOverload;
|
|
96
|
+
private _addPostfixUnaryOverload;
|
|
97
|
+
/**
|
|
98
|
+
* Extracts overload info from a property's type annotation instead of its
|
|
99
|
+
* initializer. This handles `.d.ts` declaration files where the array
|
|
100
|
+
* literal (`= [...] as const`) has been replaced by a readonly tuple type.
|
|
101
|
+
*/
|
|
102
|
+
private _addOverloadsFromTypeAnnotation;
|
|
103
|
+
/**
|
|
104
|
+
* Returns the type hierarchy chain for a given type name:
|
|
105
|
+
* [self, parent, grandparent, ...]. Primitives like "number"
|
|
106
|
+
* return a single-element array.
|
|
107
|
+
*/
|
|
108
|
+
private _getTypeChain;
|
|
109
|
+
private _minifyString;
|
|
110
|
+
/** Strips `import("...").` prefixes from fully-qualified type names for readable logs. */
|
|
111
|
+
private _shortTypeName;
|
|
112
|
+
toString(): string;
|
|
113
|
+
}
|
|
114
|
+
export {};
|