initial commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// Custom oxlint rules for Bun's built-in JavaScript (src/js/**).
|
||||
//
|
||||
// Registered via `jsPlugins` in oxlint.json. Rules are written against
|
||||
// oxlint's ESTree-compatible AST (see the `oxlint/plugins-dev` type
|
||||
// definitions). Run with `bun run lint`.
|
||||
|
||||
/**
|
||||
* Return a textual key for a simple static member expression chain made of
|
||||
* identifiers and `this`, e.g. `options.foo` or `this.a.b`. Returns `null`
|
||||
* for anything else (computed access, calls, optional chaining, literals).
|
||||
*/
|
||||
function memberExpressionKey(node) {
|
||||
if (!node || node.type !== "MemberExpression" || node.computed || node.optional) {
|
||||
return null;
|
||||
}
|
||||
const { object, property } = node;
|
||||
if (!property || property.type !== "Identifier") {
|
||||
return null;
|
||||
}
|
||||
let base;
|
||||
if (object.type === "Identifier") {
|
||||
base = object.name;
|
||||
} else if (object.type === "ThisExpression") {
|
||||
base = "this";
|
||||
} else if (object.type === "MemberExpression") {
|
||||
base = memberExpressionKey(object);
|
||||
if (base === null) return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return base + "." + property.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if `node` is the target of an assignment (simple or compound), an
|
||||
* update expression, or a `delete`. None of these can be replaced by a read
|
||||
* of a cached local.
|
||||
*/
|
||||
function isWriteTarget(node) {
|
||||
const parent = node.parent;
|
||||
if (!parent) return false;
|
||||
if (parent.type === "AssignmentExpression" && parent.left === node) return true;
|
||||
if (parent.type === "UpdateExpression" && parent.argument === node) return true;
|
||||
if (parent.type === "UnaryExpression" && parent.operator === "delete" && parent.argument === node) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if `node` is the callee of a call/new/tagged-template. Caching a
|
||||
* method in a local loses the receiver, so `obj.fn()` in the body is not
|
||||
* something a simple `const fn = obj.fn` can replace.
|
||||
*/
|
||||
function isCallee(node) {
|
||||
const parent = node.parent;
|
||||
if (!parent) return false;
|
||||
if ((parent.type === "CallExpression" || parent.type === "NewExpression") && parent.callee === node) return true;
|
||||
if (parent.type === "TaggedTemplateExpression" && parent.tag === node) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function skipKey(k) {
|
||||
return k === "parent" || k === "type" || k === "loc" || k === "range" || k === "start" || k === "end";
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect every simple static member-expression read inside the `if` test.
|
||||
* Only the outermost chain is recorded (`a.b.c`, not also `a.b`). Callees and
|
||||
* write targets are ignored: `if (obj.fn())` reads `obj.fn` but the value
|
||||
* itself isn't something a local can reuse.
|
||||
*
|
||||
* A member expression that appears as the right-hand side of an assignment
|
||||
* (`(local = obj.prop)`) is recorded in `cached` instead of `out`: that is
|
||||
* the inline cache pattern this rule recommends, so a fallback
|
||||
* `local ?? obj.prop` read in the body should not be flagged.
|
||||
*/
|
||||
function collectTestMembers(node, out, cached) {
|
||||
if (!node || typeof node !== "object") return;
|
||||
switch (node.type) {
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "ClassDeclaration":
|
||||
case "ClassExpression":
|
||||
return;
|
||||
case "MemberExpression":
|
||||
if (!isCallee(node) && !isWriteTarget(node)) {
|
||||
const key = memberExpressionKey(node);
|
||||
if (key !== null) {
|
||||
const parent = node.parent;
|
||||
if (parent && parent.type === "AssignmentExpression" && parent.operator === "=" && parent.right === node) {
|
||||
cached.add(key);
|
||||
} else if (!out.has(key)) {
|
||||
out.set(key, node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (const k in node) {
|
||||
if (skipKey(k)) continue;
|
||||
const v = node[k];
|
||||
if (Array.isArray(v)) {
|
||||
for (const child of v) {
|
||||
if (child && typeof child === "object") collectTestMembers(child, out, cached);
|
||||
}
|
||||
} else if (v && typeof v === "object" && typeof v.type === "string") {
|
||||
collectTestMembers(v, out, cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const READ = 1;
|
||||
const WRITE = 2;
|
||||
const CALLED = 4;
|
||||
|
||||
/**
|
||||
* Walk `node` collecting read/write/called flags for the static member
|
||||
* expression identified by `key`. Does not descend into nested functions or
|
||||
* classes: those run later with a different scope, so caching at the `if`
|
||||
* wouldn't help (and the value may legitimately differ by then).
|
||||
*/
|
||||
function memberAccessFlags(node, key) {
|
||||
if (!node || typeof node !== "object") return 0;
|
||||
let flags = 0;
|
||||
switch (node.type) {
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "ClassDeclaration":
|
||||
case "ClassExpression":
|
||||
return 0;
|
||||
case "MemberExpression":
|
||||
if (memberExpressionKey(node) === key) {
|
||||
if (isWriteTarget(node)) {
|
||||
// Compound assignments (`+=`, `&&=`) and `++`/`--` also read the
|
||||
// previous value, but the suggested refactor still can't
|
||||
// eliminate the write-back, so treat them purely as writes here.
|
||||
flags |= WRITE;
|
||||
} else if (isCallee(node)) {
|
||||
flags |= CALLED;
|
||||
} else {
|
||||
flags |= READ;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (const k in node) {
|
||||
if (skipKey(k)) continue;
|
||||
const v = node[k];
|
||||
if (Array.isArray(v)) {
|
||||
for (const child of v) {
|
||||
if (child && typeof child === "object") flags |= memberAccessFlags(child, key);
|
||||
}
|
||||
} else if (v && typeof v === "object" && typeof v.type === "string") {
|
||||
flags |= memberAccessFlags(v, key);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
const noDuplicateConditionalPropertyAccess = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow reading the same property in an `if` condition and again in its body. " +
|
||||
"Destructure or cache the property in a local first so the getter runs once.",
|
||||
},
|
||||
messages: {
|
||||
duplicate:
|
||||
"`{{expr}}` is read in the `if` condition and again in the body. " +
|
||||
"Read it into a local first (e.g. `const { {{prop}} } = {{base}}`) so the property is only accessed once.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
IfStatement(node) {
|
||||
const members = new Map();
|
||||
const cached = new Set();
|
||||
collectTestMembers(node.test, members, cached);
|
||||
// A property already cached via `(local = obj.prop)` in the
|
||||
// condition is the pattern this rule recommends; don't flag it.
|
||||
for (const key of cached) members.delete(key);
|
||||
if (members.size === 0) return;
|
||||
|
||||
for (const [key, member] of members) {
|
||||
const flags = memberAccessFlags(node.consequent, key);
|
||||
// If the body writes to the same property, caching it in a local
|
||||
// would change semantics (later reads would see the stale value).
|
||||
if (flags & WRITE) continue;
|
||||
// If the body calls it as a method, caching it in a local loses
|
||||
// the receiver; the simple refactor doesn't apply.
|
||||
if (flags & CALLED) continue;
|
||||
if (!(flags & READ)) continue;
|
||||
|
||||
const dot = key.lastIndexOf(".");
|
||||
context.report({
|
||||
node: member,
|
||||
messageId: "duplicate",
|
||||
data: {
|
||||
expr: key,
|
||||
prop: key.slice(dot + 1),
|
||||
base: key.slice(0, dot),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
name: "bun",
|
||||
},
|
||||
rules: {
|
||||
"no-duplicate-conditional-property-access": noDuplicateConditionalPropertyAccess,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user