Skip to content

Commit 970a1b1

Browse files
committed
chore: add no mixed nullish coalescing rule
1 parent 6e3cc1f commit 970a1b1

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

backend/__tests__/__testData__/connections.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ export async function createConnection(
55
data: Partial<ConnectionsDal.DBConnection>,
66
maxPerUser = 25,
77
): Promise<ConnectionsDal.DBConnection> {
8+
const defaultName = "user" + new ObjectId().toHexString();
89
const result = await ConnectionsDal.create(
910
{
1011
uid: data.initiatorUid ?? new ObjectId().toHexString(),
11-
name: data.initiatorName ?? "user" + new ObjectId().toHexString(),
12+
name: data.initiatorName ?? defaultName,
1213
},
1314
{
1415
uid: data.receiverUid ?? new ObjectId().toHexString(),
15-
name: data.receiverName ?? "user" + new ObjectId().toHexString(),
16+
name: data.receiverName ?? defaultName,
1617
},
1718
maxPerUser,
1819
);

packages/oxlint-config/plugin.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"rules": {
1414
"all": "off",
1515
"monkeytype-rules/no-testing-access": "error",
16+
"monkeytype-rules/no-mixed-nullish-coalescing": "error",
1617
},
1718
"overrides": [
1819
{

packages/oxlint-config/plugins/monkeytype-rules.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,56 @@ const plugin = {
198198
};
199199
},
200200
}),
201+
"no-mixed-nullish-coalescing": defineRule({
202+
createOnce(context) {
203+
/**
204+
* Returns true for expression node types that, when combined with ??
205+
* without explicit parentheses, create confusing/ambiguous precedence.
206+
* Excluded: UnaryExpression (clearly bound to its operand),
207+
* LogicalExpression with ?? (same operator, unambiguous).
208+
*/
209+
const isParenthesized = (node, source) => {
210+
// OXC strips ParenthesizedExpression from the AST before visiting,
211+
// so check the raw source surrounding the node's range instead.
212+
const start = node.range?.[0] ?? node.start;
213+
const end = node.range?.[1] ?? node.end;
214+
return source[start - 1] === "(" && source[end] === ")";
215+
};
216+
217+
const isMixedOperatorNode = (node, source) => {
218+
if (isParenthesized(node, source)) return false;
219+
return (
220+
node.type === "BinaryExpression" ||
221+
(node.type === "LogicalExpression" && node.operator !== "??") ||
222+
node.type === "ConditionalExpression"
223+
);
224+
};
225+
226+
return {
227+
LogicalExpression(node) {
228+
if (node.operator !== "??") return;
229+
230+
const source = context.sourceCode.getText();
231+
232+
if (isMixedOperatorNode(node.left, source)) {
233+
context.report({
234+
node: node.left,
235+
message:
236+
"Nullish coalescing (`??`) mixed with other operators without explicit parentheses. Extract to a helper variable or wrap in parentheses for clarity.",
237+
});
238+
}
239+
240+
if (isMixedOperatorNode(node.right, source)) {
241+
context.report({
242+
node: node.right,
243+
message:
244+
"Nullish coalescing (`??`) mixed with other operators without explicit parentheses. Extract to a helper variable or wrap in parentheses for clarity.",
245+
});
246+
}
247+
},
248+
};
249+
},
250+
}),
201251
"component-pascal-case": defineRule({
202252
createOnce(context) {
203253
const isPascalCase = (name) => /^[A-Z][a-zA-Z0-9]*$/.test(name);

0 commit comments

Comments
 (0)