From 1a8c2b3a280f9430d2a0b1cc3d3d7ad56389420d Mon Sep 17 00:00:00 2001 From: Harshal Devmurari <92778686+harshal015@users.noreply.github.com> Date: Mon, 25 May 2026 12:37:03 +0530 Subject: [PATCH 1/2] add new rule --- packages/core/src/index.js | 2 + .../rules/require-test-assignee-comment.js | 67 +++++++++++++++++++ .../require-test-assignee-comment.test.js | 39 +++++++++++ .../rules/require-test-assignee-comment.md | 52 ++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 packages/core/src/rules/require-test-assignee-comment.js create mode 100644 packages/core/src/tests/require-test-assignee-comment.test.js create mode 100644 packages/docs/rules/require-test-assignee-comment.md diff --git a/packages/core/src/index.js b/packages/core/src/index.js index b629ac0..d9b666c 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -4,6 +4,7 @@ import noDatepipeTransformFormatArg from './rules/no-datepipe-transform-format-a import noInlineDateFormat from './rules/no-inline-date-format.js'; import noHardcodedStrings from './rules/no-hardcoded-strings.js'; import noDateCurrencyMutation from './rules/no-date-currency-mutation.js'; +import requireTestAssigneeComment from './rules/require-test-assignee-comment.js'; const rules = { 'i18n-key-naming-convention': i18nKeyNamingConvention, @@ -12,6 +13,7 @@ const rules = { 'no-inline-date-format': noInlineDateFormat, 'no-hardcoded-strings': noHardcodedStrings, 'no-date-currency-mutation': noDateCurrencyMutation, + 'require-test-assignee-comment': requireTestAssigneeComment, }; const configs = { diff --git a/packages/core/src/rules/require-test-assignee-comment.js b/packages/core/src/rules/require-test-assignee-comment.js new file mode 100644 index 0000000..470dfa6 --- /dev/null +++ b/packages/core/src/rules/require-test-assignee-comment.js @@ -0,0 +1,67 @@ +import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator( + () => 'https://github.com/fylein/fyle-eslint-plugin/blob/main/packages/docs/rules/require-test-assignee-comment.md', +); + +const RULE_NAME = 'require-test-assignee-comment'; + +const ASSIGNEE_COMMENT_PATTERN = /Assignee:\s+@[\w.-]+/i; + +export default createRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: 'Require an explicit assignee comment for Playwright `test.fixme` and `test.fail` cases.', + recommended: 'recommended', + }, + schema: [], + messages: { + missingAssignee: + 'test.fixme/test.fail must include an explicit assignee comment immediately above the call, e.g. "// @assignee @username".', + }, + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + + function isTestFixmeOrFailCall(node) { + if (node.type !== AST_NODE_TYPES.CallExpression) { + return false; + } + + const callee = node.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const object = callee.object; + const property = callee.property; + if (object.type !== AST_NODE_TYPES.Identifier || object.name !== 'test') { + return false; + } + + return property.type === AST_NODE_TYPES.Identifier && (property.name === 'fixme' || property.name === 'fail'); + } + + function hasAssigneeComment(node) { + const statement = node.parent && node.parent.type === AST_NODE_TYPES.ExpressionStatement ? node.parent : node; + const leadingComments = sourceCode.getCommentsBefore(statement); + + return leadingComments.some((comment) => ASSIGNEE_COMMENT_PATTERN.test(comment.value)); + } + + return { + CallExpression(node) { + if (!isTestFixmeOrFailCall(node)) { + return; + } + + if (!hasAssigneeComment(node)) { + context.report({ node, messageId: 'missingAssignee' }); + } + }, + }; + }, +}); diff --git a/packages/core/src/tests/require-test-assignee-comment.test.js b/packages/core/src/tests/require-test-assignee-comment.test.js new file mode 100644 index 0000000..e442fc4 --- /dev/null +++ b/packages/core/src/tests/require-test-assignee-comment.test.js @@ -0,0 +1,39 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import tsParser from '@typescript-eslint/parser'; +import rule from '../rules/require-test-assignee-comment.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}); + +ruleTester.run('require-test-assignee-comment', rule, { + valid: [ + `// Assignee: @arjun\n test.fixme('known issue', async ({ page }) => {});`, + `test('works normally', async ({ page }) => {});`, + `test.skip('skipped test without fail', async ({ page }) => {});`, + ], + invalid: [ + { + code: `test.fixme('known issue', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `// TODO: temporary bug\n test.fail('unstable feature', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `// @owner @aniruddha\n test.fail('should retry after failure', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + { + code: `/* @responsible @omkar */\n test.fail('known bug', async ({ page }) => {});`, + errors: [{ messageId: 'missingAssignee' }], + }, + ], +}); diff --git a/packages/docs/rules/require-test-assignee-comment.md b/packages/docs/rules/require-test-assignee-comment.md new file mode 100644 index 0000000..0f1e43f --- /dev/null +++ b/packages/docs/rules/require-test-assignee-comment.md @@ -0,0 +1,52 @@ +# Custom ESLint Rule: require-test-assignee-comment + +This rule ensures that Playwright `test.fixme` and `test.fail` annotations are never left orphaned by requiring an explicit assignee comment. + +## What it checks + +- `test.fixme(...)` +- `test.fail(...)` + +The rule reports when these calls are present without a nearby comment assigning ownership. + +## Accepted comment formats + +Valid examples: + +```js +// Assignee: @username +``` + +Only the `@assignee` syntax is accepted by this rule. + +## Example + +### ✅ Valid + +```js +// Assignee: @arjun +test.fixme('known issue', async ({ page }) => {}); +``` + +### ❌ Invalid + +```js +test.fixme('known issue', async ({ page }) => {}); +``` + +## Configuration + +```js +import fyleCore from '@fyle/eslint-plugin'; + +export default [ + { + plugins: { + '@fyle': fyleCore, + }, + rules: { + '@fyle/require-test-assignee-comment': 'error', + }, + }, +]; +``` From 4d1fff7cdc7d1811c102d772dcf9261177a6ac75 Mon Sep 17 00:00:00 2001 From: Harshal Devmurari <92778686+harshal015@users.noreply.github.com> Date: Mon, 25 May 2026 12:50:32 +0530 Subject: [PATCH 2/2] add rule --- packages/core/src/index.js | 2 + .../src/rules/require-date-day-two-digits.js | 75 +++++++++++++++++++ .../tests/require-date-day-two-digits.test.js | 38 ++++++++++ .../docs/rules/require-date-day-two-digits.md | 27 +++++++ 4 files changed, 142 insertions(+) create mode 100644 packages/core/src/rules/require-date-day-two-digits.js create mode 100644 packages/core/src/tests/require-date-day-two-digits.test.js create mode 100644 packages/docs/rules/require-date-day-two-digits.md diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d9b666c..635a643 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -5,6 +5,7 @@ import noInlineDateFormat from './rules/no-inline-date-format.js'; import noHardcodedStrings from './rules/no-hardcoded-strings.js'; import noDateCurrencyMutation from './rules/no-date-currency-mutation.js'; import requireTestAssigneeComment from './rules/require-test-assignee-comment.js'; +import requireDateDayTwoDigits from './rules/require-date-day-two-digits.js'; const rules = { 'i18n-key-naming-convention': i18nKeyNamingConvention, @@ -14,6 +15,7 @@ const rules = { 'no-hardcoded-strings': noHardcodedStrings, 'no-date-currency-mutation': noDateCurrencyMutation, 'require-test-assignee-comment': requireTestAssigneeComment, + 'require-date-day-two-digits': requireDateDayTwoDigits, }; const configs = { diff --git a/packages/core/src/rules/require-date-day-two-digits.js b/packages/core/src/rules/require-date-day-two-digits.js new file mode 100644 index 0000000..5f1d212 --- /dev/null +++ b/packages/core/src/rules/require-date-day-two-digits.js @@ -0,0 +1,75 @@ +import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; + +const createRule = ESLintUtils.RuleCreator( + () => 'https://github.com/fylein/fyle-eslint-plugin/blob/main/packages/docs/rules/require-date-day-two-digits.md', +); + +const RULE_NAME = 'require-date-day-two-digits'; + +export default createRule({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + "Require 'day' option to use '2-digit' when calling Date.prototype.toLocaleDateString to ensure two-digit day formatting (e.g., '01 Mar' not '1 Mar').", + recommended: 'recommended', + }, + schema: [], + messages: { + requireTwoDigit: "Use day: '2-digit' in toLocaleDateString options to produce two-digit day values.", + }, + }, + defaultOptions: [], + create(context) { + function isToLocaleDateStringCall(node) { + if (node.type !== AST_NODE_TYPES.CallExpression) return false; + const callee = node.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) return false; + const prop = callee.property; + if (prop.type !== AST_NODE_TYPES.Identifier) return false; + return prop.name === 'toLocaleDateString'; + } + + return { + CallExpression(node) { + if (!isToLocaleDateStringCall(node)) return; + + // Determine which argument is the options object. + // toLocaleDateString(locales?, options?) -> options is arg[1] if arg0 is string/array, else arg[0] if it's an object + const args = node.arguments || []; + let optionsArg = null; + if (args.length >= 2) { + optionsArg = args[1]; + } else if (args.length === 1) { + const first = args[0]; + if (first.type === AST_NODE_TYPES.ObjectExpression) optionsArg = first; + } + + if (!optionsArg) return; + + // If optionsArg is not object literal, skip + if (optionsArg.type !== AST_NODE_TYPES.ObjectExpression) return; + + // Find day property + for (const prop of optionsArg.properties) { + if (prop.type !== AST_NODE_TYPES.Property) continue; + const key = prop.key; + const value = prop.value; + const keyName = + key.type === AST_NODE_TYPES.Identifier + ? key.name + : key.type === AST_NODE_TYPES.Literal + ? String(key.value) + : null; + if (keyName === 'day') { + if (value.type === AST_NODE_TYPES.Literal && value.value === 'numeric') { + context.report({ node: value, messageId: 'requireTwoDigit' }); + } + break; + } + } + }, + }; + }, +}); diff --git a/packages/core/src/tests/require-date-day-two-digits.test.js b/packages/core/src/tests/require-date-day-two-digits.test.js new file mode 100644 index 0000000..281d57f --- /dev/null +++ b/packages/core/src/tests/require-date-day-two-digits.test.js @@ -0,0 +1,38 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import tsParser from '@typescript-eslint/parser'; +import rule from '../rules/require-date-day-two-digits.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, +}); + +ruleTester.run('require-date-day-two-digits', rule, { + valid: [ + `new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })`, + `new Date().toLocaleDateString({ month: 'short', day: '2-digit' })`, + // no day option + `new Date().toLocaleDateString('en-US', { month: 'short', year: 'numeric' })`, + // non-literal day value (can't assert) + `const d = 'numeric'; new Date().toLocaleDateString('en-US', { day: d })`, + ], + invalid: [ + { + code: `new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })`, + errors: [{ messageId: 'requireTwoDigit' }], + }, + { + code: `new Date().toLocaleDateString({ day: 'numeric' })`, + errors: [{ messageId: 'requireTwoDigit' }], + }, + { + code: `new Date().toLocaleDateString('en-GB', { day: 'numeric' })`, + errors: [{ messageId: 'requireTwoDigit' }], + }, + ], +}); diff --git a/packages/docs/rules/require-date-day-two-digits.md b/packages/docs/rules/require-date-day-two-digits.md new file mode 100644 index 0000000..3eec6be --- /dev/null +++ b/packages/docs/rules/require-date-day-two-digits.md @@ -0,0 +1,27 @@ +# Custom ESLint Rule: require-date-day-two-digits + +This rule enforces using `day: '2-digit'` when calling `Date.prototype.toLocaleDateString` so date strings always use two-digit day representations (e.g., `01 Mar` instead of `1 Mar`). + +## What it checks + +- Calls to `toLocaleDateString` with an object literal options argument containing `day: 'numeric'` are reported. +- Cases where `day` is missing or uses `'2-digit'` are allowed. +- If the `day` value is a non-literal (variable/expression), the rule does not attempt to assert and will not report. + +## Example + +### ✅ Valid + +```js +new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }); +``` + +### ❌ Invalid + +```js +new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +``` + +## Rationale + +E2E tests should match the application's date formatting which uses two-digit days. Using `day: 'numeric'` produces single-digit days for the 1st–9th days, causing intermittent failures when tests expect two-digit formatting.