Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ This package is currently on v3, which uses ESLint 9's flat config format. The p
- v1 β†’ v2: See `docs/MIGRATION-V2.md`
- v2 β†’ v3: See `docs/MIGRATION-V3.md` (ESLint 9 flat config migration requiring Node.js 24+)

## Versioning Convention

When adding rules to `configs.recommended` in the `hs-web-team` plugin (or any other bundled config):

- **`'warn'` severity** β†’ minor bump (`feat:` commit). Warn-only rules don't fail standard CI, so consumers aren't broken without opt-in (`--max-warnings 0`).
- **`'error'` severity** β†’ breaking change (`feat!:` commit, major bump). Error rules can fail CI for any consumer with a violation, regardless of their settings.

## Important Notes

- This package supports multiple use cases:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"./cypress": {
"types": "./cypress/index.d.ts",
"default": "./cypress/index.js"
}
},
"./plugin": "./plugins/hs-web-team/index.js"
},
"typesVersions": {
"*": {
Expand Down
3 changes: 3 additions & 0 deletions plugins/hs-web-team/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { noAbbreviations } from './rules/no-abbreviations.js';
import { noReduceAccumulatorCopy } from './rules/no-reduce-accumulator-copy.js';

export const hsWebTeamPlugin = {
rules: {
'no-abbreviations': noAbbreviations,
'no-reduce-accumulator-copy': noReduceAccumulatorCopy,
},
};
Expand All @@ -12,6 +14,7 @@ hsWebTeamPlugin.configs = {
recommended: {
plugins: { 'hs-web-team': hsWebTeamPlugin },
rules: {
'hs-web-team/no-abbreviations': 'warn',
'hs-web-team/no-reduce-accumulator-copy': 'error',
},
},
Expand Down
135 changes: 135 additions & 0 deletions plugins/hs-web-team/rules/no-abbreviations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const DEFAULT_EXCEPTIONS = new Set(['e', '_']);
const DEFAULT_MIN_LENGTH = 2;

const SORT_METHODS = new Set(['sort', 'toSorted']);
const ARRAY_ITERATION_METHODS = new Set([
'map',
'flatMap',
'forEach',
'filter',
'find',
'findIndex',
'findLast',
'findLastIndex',
'some',
'every',
// reduce/reduceRight are intentionally excluded: their callback signature is
// (accumulator, currentValue, currentIndex, …), so position 1 is the current
// *value*, not the index. The i-at-position-1 heuristic does not apply.
]);

/**
* Returns true if the VariableDeclarator is in the init of a for/for...of/for...in statement.
* Walks the parent chain directly β€” no scope API needed.
*
* @param {import('eslint').Rule.Node} declaratorNode - VariableDeclarator node
*/
function isDeclaredInForLoopInit(declaratorNode) {
const declaration = declaratorNode.parent; // VariableDeclaration
const enclosing = declaration?.parent;
if (!enclosing) return false;
return (
(enclosing.type === 'ForStatement' && enclosing.init === declaration) ||
enclosing.type === 'ForInStatement' ||
enclosing.type === 'ForOfStatement'
);
}

/**
* Returns true if funcNode is a direct inline callback to a method whose name is in methodNames.
* Only matches non-computed method calls (arr.sort(...), not arr['sort'](...)).
*
* @param {import('eslint').Rule.Node} funcNode - ArrowFunctionExpression or FunctionExpression
* @param {Set<string>} methodNames
*/
function isCallbackToMethod(funcNode, methodNames) {
const callExpr = funcNode.parent;
if (callExpr?.type !== 'CallExpression') return false;
if (!callExpr.arguments.includes(funcNode)) return false;
const { callee } = callExpr;
return (
callee?.type === 'MemberExpression' &&
!callee.computed &&
methodNames.has(callee.property.name)
);
}

/** @type {import('eslint').Rule.RuleModule} */
export const noAbbreviations = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow abbreviated identifier names (e.g. `w` instead of `warnings`)',
url: 'https://docs.hubwt.com/docs/developers/coding-on-the-web-team/coding-standards/javascript-standards/#avoid-abbreviation',
recommended: true,
},
messages: {
tooShort:
'Identifier "{{name}}" is too short ({{length}} < {{min}}). Use a descriptive name.',
},
schema: [
{
type: 'object',
properties: {
exceptions: { type: 'array', items: { type: 'string' }, uniqueItems: true },
minLength: { type: 'integer', minimum: 1 },
},
additionalProperties: false,
},
],
},

create(context) {
const options = context.options[0] ?? {};
const exceptions = new Set([...DEFAULT_EXCEPTIONS, ...(options.exceptions ?? [])]);
const minLength = options.minLength ?? DEFAULT_MIN_LENGTH;

return {
Identifier(node) {
const { name, parent } = node;

if (name.length >= minLength) return;
if (exceptions.has(name)) return;

// Only flag declaration sites β€” not usage sites.
//
// Object destructuring shorthand (const { w } = response) is intentionally
// excluded: `w` may be an external API property name the developer doesn't
// control. The idiomatic fix β€” const { w: warning } = response β€” is correct
// but non-obvious enough to leave as opt-in. Array destructuring IS included
// because the element binding name is always chosen by the developer.
const isDeclaration =
(parent.type === 'VariableDeclarator' && parent.id === node) ||
(parent.type === 'ArrayPattern' && parent.elements.includes(node)) ||
(parent.type === 'FunctionDeclaration' && parent.id === node) ||
(parent.type === 'FunctionDeclaration' && parent.params.includes(node)) ||
(parent.type === 'ArrowFunctionExpression' && parent.params.includes(node)) ||
(parent.type === 'FunctionExpression' && parent.params.includes(node));

if (!isDeclaration) return;

// For-loop variables (i, j, k…) are exempt regardless of length.
if (parent.type === 'VariableDeclarator' && isDeclaredInForLoopInit(parent)) return;

// a and b are conventional sort-comparator names β€” exempt only inside .sort()/.toSorted().
if ((name === 'a' || name === 'b') && isCallbackToMethod(parent, SORT_METHODS)) return;

// i is a conventional index name β€” exempt only when it is the index param (position 1)
// of an array iteration callback: items.map((item, i) => ...).
// Position 1 is the index slot for every array iteration method except reduce/reduceRight
// (where it's position 2), but naming the reduce index `i` is uncommon enough to skip.
if (
name === 'i' &&
parent.params?.indexOf(node) === 1 &&
isCallbackToMethod(parent, ARRAY_ITERATION_METHODS)
) return;

context.report({
node,
messageId: 'tooShort',
data: { name, length: name.length, min: minLength },
});
},
};
},
};
Loading