fix(linter/plugins): make spreading Comment instances keep loc property#22238
fix(linter/plugins): make spreading Comment instances keep loc property#22238KuSh wants to merge 5 commits into
Conversation
bf5ebe9 to
14e310b
Compare
|
Thanks for this. Have you by any chance measured perf before/after this change? Unfortunately we don't have any JS benchmarks at present, so we measure on a rather ad hoc basis, e.g. running Oxlint with some JS plugins on VS Code repo. I'm asking because generally we've tried to avoid |
No, but if the procedure is written somewhere I could do that. I've tried to find another way but couldn't come up with another one, but will be willing to try if you have some in mind. Do you know some jsPlugins that exercise comments? unicorn doesn't work without that change so I can't compare with before. |
14e310b to
d6eb543
Compare
|
@overlookmotel did bench the branch with eslint-comments (thanks HansAuger on discord for the suggestion) and a custom plugin to specifically stress loc access: Bench ResultsBenchmarked on VS Code repo (~10,500 files, 24 threads), 5 runs with 2 warmup each using hyperfine. stress-loc (10× .loc access per comment — amplifies overhead)
eslint-comments (7 real rules — no-use, disable-enable-pair, etc.)
VerdictNo measurable regression. Both differences are within noise (standard deviations overlap). The Proxy overhead on The fix is safe to land from a performance standpoint. Configuration used:Node v24.14.0 stress-locimport type { Comment, Plugin, Rule } from "#oxlint/plugins";
const stressLocRule: Rule = {
create(context) {
const { sourceCode } = context;
let programNode: any;
let totalComments = 0;
return {
Program(node) {
programNode = node;
const comments = sourceCode.getAllComments();
for (const comment of comments) {
totalComments++;
// 10 iterations per comment to amplify any overhead signal.
for (let i = 0; i < 10; i++) {
const loc = comment.loc;
const _startLine = loc.start.line;
const _startColumn = loc.start.column;
const _endLine = loc.end.line;
const _endColumn = loc.end.column;
}
const _type = comment.type;
const _value = comment.value;
const _range = comment.range;
}
},
"Program:exit"() {
if (totalComments > 0 && programNode) {
context.report({ node: programNode, message: `processed ${totalComments} comments` });
}
},
};
},
};
const plugin: Plugin = {
meta: { name: "comment-perf" },
rules: { "stress-loc": stressLocRule },
};
export default plugin;Configured with: {
"jsPlugins": ["./test/fixtures/comment_perf/plugin.ts"],
"rules": {
"comment-perf/stress-loc": "error"
}
}eslint-commentsInstalled as {
"jsPlugins": [{ "name": "eslint-comments", "specifier": "@eslint-community/eslint-plugin-eslint-comments" }],
"rules": {
"eslint-comments/disable-enable-pair": "error",
"eslint-comments/no-duplicate-disable": "error",
"eslint-comments/no-unlimited-disable": "error",
"eslint-comments/no-restricted-disable": "error",
"eslint-comments/no-use": "error",
"eslint-comments/require-description": "error",
"eslint-comments/no-aggregating-enable": "error"
// Skipped no-unused-disable (heavy patching, likely incompatible).
}
} |
d6eb543 to
a261679
Compare
|
If you have time, could you try altering the stress rule to this: const stressLocRule: Rule = {
create(context) {
const { sourceCode } = context;
return {
Program(node) {
const comments = sourceCode.getAllComments();
let count = 0;
// 10 iterations per comment to amplify any overhead signal
for (let i = 0; i < 10; i++) {
for (const comment of comments) {
const loc = comment.loc;
count += loc.start.line;
count += loc.start.column;
count += loc.end.line;
count += loc.end.column;
count += comment.type.length;
count += comment.value.length;
count += comment.range[0];
count += comment.range[1];
}
}
if (count > 0) {
context.report({ node, message: `got ${count} count` });
}
},
};
},
};In your original version, possible that that
Also, if I've understood |
Microbenchmark Resultssource: https://gist.github.com/KuSh/7af1829312429dd6e638a47de430a86a No reset (steady state after warmup)
With reset (per file cycle)
ConclusionsWithout reset, SR ≈ B/E — all three within noise. After warmup, SR's self-replaced data properties are as fast as B/E's getter + With reset, SR collapses — B/E are the only viable approach — correct spread behavior, cheap reset ( |
Complete Benchmark Results
The stress file isolates just the |
|
Excellent work! Thanks loads for digging deep into this. I have to say I'm mystified why "B:modvar" is performing better than "C:proto" with reset. As I mentioned on Discord, I'm mostly away from keyboard until next week. I'll take a closer look as soon as I'm back in the saddle. Again, thanks loads for getting into this in depth. For the record, more context here: https://discord.com/channels/1079625926024900739/1080712072012238858/threads/1508788046110396476 |
e527b4d to
ef640fb
Compare
…lugin
Adds a test fixture to exercise the jsPlugin infrastructure with the
real eslint-plugin-unicorn package. This test exposes a bug where
context.report({ loc: ..., messageId: ... }) fails with 'Either node
or loc is required'.
Changes:
- Add eslint-plugin-unicorn as dev dependency to apps/oxlint
- Add test fixture at test/fixtures/unicorn_expiring_todo_comments/
- Use alias 'unicorn-js' since 'unicorn' is reserved for native rules
- Replace private #loc field with WeakMap storage - Add Proxy in constructor to make loc enumerable for spread - Fix unicorn/expiring-todo-comments rule failing with spread comments
Replace Proxy (ownKeys/getOwnPropertyDescriptor traps) with Object.defineProperty + #loc private field for Comment.loc. Variant B: static #LOC_DESC shared getter via Object.defineProperty in constructor, #loc private field caches computed Location. Reset is just #loc = null. Benchmarks (VS Code src/, 10 rules x 10 iterations): Proxy (A): 21.07s Variant B: 20.16s (4.5% faster)
ef640fb to
e89fee8
Compare
|
I'm going through this now. Have moved the docs corrections to #22891, since they're orthogonal to this PR (so deleted last commit from this PR), and rebased on main. Hope you don't mind me fiddling with your work. |
|
No problem at all! Thanks for that |
|
OK, I've dug into this. Firstly, there was a problem with the benchmark. If you changed the order of the variants, the results changed. Claude's explanation:
Adapted version which fixes this: https://gist.github.com/overlookmotel/9687d97e8decbb08787e03cc2e888bdb Results
Run on Mac Mini M4 Pro. Upshot
"B:modvar" is 13.5x slower to construct a However, that doesn't matter too much! As So, in short, I think we can do this! Next stepsCodeI think variant D is the winner. That's IMO what we should go with in this PR. Note: The weird pattern with copying the functions defined in the static block into If I may ask, please try to avoid any extraneous changes from the previous versions surviving. Small diff is the name of the game, and AI doesn't seem to always get that. TestsCould you please reduce the test case to not require Or testing the spread behavior could probably be folded into one of the existing fixtures that tests comments, rather than introducing a new fixture. TokensWe should probably apply the same change to @KuSh If you can be bothered, that'd be great (in a separate PR). Otherwise no worries, I'll do it. Note: Unfortunately we should not apply this change to AST nodes too. AST node objects are not pooled, so the increased construction cost would be a killer. We'll have to find another way to tackle that. Store
|
Summary: Fixing unicorn/expiring-todo-comments via jsPlugin
Problem
The
unicorn/expiring-todo-commentsrule failed when run through oxlint's jsPlugin system with error:TypeError: Either `node` or `loc` is requiredThe rule spreads Comment instances (
{...comment}) to process each line of multi-line comments, but lost thelocproperty because:locwas a prototype getter (not an own enumerable property)Solution
Two-part fix in
apps/oxlint/src-js/plugins/comments.ts:#locfield)locenumerable for spread)ownKeys: includes "loc" in enumerable propertiesgetOwnPropertyDescriptor: returns getter descriptor for "loc"Why This Approach
#locthrough Proxy from external debug/reset codeBonus: Removed toJSON Hack
Before: A
toJSON()method was needed to includelocinJSON.stringifybecause:locwas a prototype getter (not an own property)JSON.stringifyonly copy own propertiestoJSON()manually addedlocto serialized outputAfter: The Proxy makes
locnaturally enumerable, sotoJSON()is unnecessary and was removed.Files Changed
apps/oxlint/src-js/plugins/comments.ts- Main fixapps/oxlint/package.json- Added eslint-plugin-unicorn dev dependencyapps/oxlint/test/fixtures/unicorn_expiring_todo_comments/Test Results