Files
oxc-project__oxc/apps/oxlint/tsdown_plugins/inline_search.ts
renovate[bot] bcefc00974 chore(deps): update oxc to ^0.136.0 (#23448)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Adoption](https://docs.renovatebot.com/merge-confidence/) |
[Passing](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|---|---|
| [@oxc-project/types](https://oxc.rs)
([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxc-types))
| [`^0.135.0` →
`^0.136.0`](https://renovatebot.com/diffs/npm/@oxc-project%2ftypes/0.135.0/0.136.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@oxc-project%2ftypes/0.136.0?slim=true)
|
![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@oxc-project%2ftypes/0.136.0?slim=true)
|
![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@oxc-project%2ftypes/0.135.0/0.136.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@oxc-project%2ftypes/0.135.0/0.136.0?slim=true)
|
| [oxc-parser](https://oxc.rs/docs/guide/usage/parser)
([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/napi/parser))
| [`^0.135.0` →
`^0.136.0`](https://renovatebot.com/diffs/npm/oxc-parser/0.135.0/0.136.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/oxc-parser/0.136.0?slim=true)
|
![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxc-parser/0.136.0?slim=true)
|
![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxc-parser/0.135.0/0.136.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxc-parser/0.135.0/0.136.0?slim=true)
|

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxc-parser)</summary>

###
[`v0.136.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/napi/parser/CHANGELOG.md#01360---2026-06-15)

##### ⚡ Performance

-
[`da1a6c6`](https://redirect.github.com/oxc-project/oxc/commit/da1a6c6)
diagnostics: Migrate to allocation-optimized oxc-miette
([#&#8203;23094](https://redirect.github.com/oxc-project/oxc/issues/23094))
(Boshen)

</details>

---

### Configuration

📅 **Schedule**: (in timezone Asia/Shanghai)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/oxc-project/oxc).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMTkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIxOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
2026-06-15 14:31:57 +01:00

240 lines
8.2 KiB
TypeScript

import fs from "node:fs";
import { join as pathJoin } from "node:path";
import { Visitor } from "oxc-parser";
import { parse } from "./utils.ts";
import type { Plugin } from "rolldown";
import type * as ESTree from "oxc-parser";
// Name of binary search function to inline
const INLINE_FUNC_NAME = "firstTokenAtOrAfter";
// Path of file the binary search function is defined in
const INLINE_FUNC_PATH = pathJoin(import.meta.dirname, "../src-js/plugins/tokens_methods.ts");
// Files to inline the binary search function into
const FILES = ["/src-js/plugins/tokens_methods.ts", "/src-js/plugins/comments_methods.ts"];
// Get details of the function to be inlined
const { fnParams, returnParamIndex, fnBodySource } = extractInlinedFunction(
INLINE_FUNC_PATH,
INLINE_FUNC_NAME,
);
/**
* Plugin to inline calls to binary search helper function `firstTokenAtOrAfter` into its call sites.
*
* This eliminates function call overhead for the binary search used by all token/comment methods.
* The function definition is read from the source file, so edits to the helper function are automatically
* reflected in the inlined output.
*
* The function is inlined into all call sites in `FILES` list above.
*
* ```ts
* // Original code
* const index = firstTokenAtOrAfter(int32, rangeStart, searchFromIndex, length);
*
* // After transform
* let index = searchFromIndex;
* for (let endIndex = length; index < endIndex; ) {
* const mid = (index + endIndex) >> 1;
* if (int32[mid << 2] < rangeStart) {
* index = mid + 1;
* } else {
* endIndex = mid;
* }
* }
* ```
*/
const plugin: Plugin = {
name: "inline-binary-search",
transform: {
filter: {
id: new RegExp("(?:" + FILES.map((filename) => RegExp.escape(filename)).join("|") + ")$"),
},
handler(code, path, meta) {
const magicString = meta.magicString!;
const program = parse(path, code);
// Set of call expression nodes that have been inlined
const inlinedCallExprs = new Set<ESTree.CallExpression>();
/**
* Convert offset to line number.
* @param offset - Offset in source code
* @returns Line number
*/
function lineNumber(offset: number): number {
let line = 1;
for (let i = 0; i < offset; i++) {
if (code[i] === "\n") line++;
}
return line;
}
// Visit AST.
// Inline call sites and check for any calls that could not be inlined.
const visitor = new Visitor({
VariableDeclaration(varDecl) {
if (varDecl.declarations.length !== 1) return;
const declarator = varDecl.declarations[0];
if (declarator.id.type !== "Identifier" || !declarator.init) return;
let declKind = varDecl.kind;
if (!["const", "let", "var"].includes(declKind)) return;
if (declKind === "const") declKind = "let";
let callNode: ESTree.CallExpression;
let suffix: string | null = null;
const { init } = declarator;
if (isTargetCall(init)) {
callNode = init;
} else if (init.type === "BinaryExpression" && isTargetCall(init.left)) {
callNode = init.left;
// e.g. " - 1" from `firstTokenAtOrAfter(...) - 1`
suffix = code.slice(callNode.end, init.end);
} else {
return;
}
const args = callNode.arguments.map((arg) => {
if (arg.type !== "Identifier" && arg.type !== "Literal") {
throw new Error(
`Unexpected parameter type in \`${INLINE_FUNC_NAME}\` call ` +
`at line ${lineNumber(arg.start)}: ${arg.type}`,
);
}
return code.slice(arg.start, arg.end);
});
if (args.length !== fnParams.length) {
throw new Error(
`\`${INLINE_FUNC_NAME}\` called with ${args.length} args, expected ${fnParams.length} ` +
`at line ${lineNumber(callNode.start)}`,
);
}
// Build replacement.
// `let <varName> = <returnParamArg>;`
const varName = declarator.id.name;
let replacement = `${declKind} ${varName} = ${args[returnParamIndex]};\n`;
// Build inlined body by replacing parameter names with argument expressions.
// The return parameter is replaced with the declared variable name.
let inlined = fnBodySource;
for (let i = 0; i < fnParams.length; i++) {
const replacementVar = i === returnParamIndex ? varName : args[i];
inlined = inlined.replace(new RegExp(`\\b${fnParams[i]}\\b`, "g"), replacementVar);
}
replacement += inlined;
// If there's a suffix (e.g. ` - 1`), append `<varName> = <varName><suffix>;`
if (suffix !== null) replacement += `\n${varName} = ${varName}${suffix};`;
magicString.overwrite(varDecl.start, varDecl.end, replacement);
// Record the call expression, so `CallExpression` visitor doesn't throw an error when visiting it
inlinedCallExprs.add(callNode);
},
CallExpression(callExpr) {
if (isTargetCall(callExpr) && !inlinedCallExprs.has(callExpr)) {
throw new Error(
`\`${INLINE_FUNC_NAME}\` call on line ${lineNumber(callExpr.start)} could not be inlined. ` +
"All calls must be in a variable declaration.",
);
}
},
});
visitor.visit(program);
// Check some calls were inlined. If there weren't, probably paths in `FILES` are wrong.
if (inlinedCallExprs.size === 0) {
throw new Error(`No \`${INLINE_FUNC_NAME}\` calls found in ${path}`);
}
return { code: magicString };
},
},
};
export default plugin;
/**
* Check if a node is a call to the function to be inlined.
* @param node - AST node to check
* @returns `true` if `node` is a call to the function to be inlined
*/
function isTargetCall(node: ESTree.Node): node is ESTree.CallExpression {
return (
node.type === "CallExpression" &&
node.callee.type === "Identifier" &&
node.callee.name === INLINE_FUNC_NAME
);
}
/**
* Parse the function to be inlined by plugin.
* Extracts function parameter names, the return parameter index, and the body source text.
*
* @param path - Path to file the function is defined in
* @param funcName - Name of the function to find
*/
function extractInlinedFunction(
path: string,
funcName: string,
): { fnParams: string[]; returnParamIndex: number; fnBodySource: string } {
const code = fs.readFileSync(path, "utf8");
const program = parse(path, code);
// Find the function declaration
let funcDecl: ESTree.Function | undefined;
for (const stmt of program.body) {
let maybeFuncDecl: ESTree.Statement | ESTree.Declaration = stmt;
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration !== null) {
maybeFuncDecl = stmt.declaration;
}
if (maybeFuncDecl.type === "FunctionDeclaration" && maybeFuncDecl.id?.name === funcName) {
funcDecl = maybeFuncDecl;
break;
}
}
if (!funcDecl) throw new Error(`Failed to find function \`${funcName}\``);
// Get function parameter names
const fnParams = funcDecl.params.map((param) => {
if (param.type !== "Identifier") {
throw new Error(`Unexpected parameter type in \`${funcName}\`: ${param.type}`);
}
return param.name;
});
// Find return statement and the function param that matches it
const { body } = funcDecl;
if (body === null) throw new Error(`\`${funcName}\` has no body`);
const lastStmt = body.body.at(-1);
if (
!lastStmt ||
lastStmt.type !== "ReturnStatement" ||
lastStmt.argument?.type !== "Identifier"
) {
throw new Error(`\`${funcName}\` must end with \`return <identifier>;\``);
}
const returnParamName = lastStmt.argument.name;
const returnParamIndex = fnParams.indexOf(returnParamName);
if (returnParamIndex === -1) {
throw new Error(`Return value \`${returnParamName}\` is not a parameter of \`${funcName}\``);
}
// Get function body's code to be inlined - everything from after `{` to before `return ...`)
const fnBodySource = code.slice(body.start + 1, lastStmt.start).trim();
return { fnParams, returnParamIndex, fnBodySource };
}