From f9b47dbaf2263279207aea440deb14196d278fab Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:12:14 -0400 Subject: [PATCH 1/2] Extract rule: template-require-form-method --- README.md | 1 + docs/rules/template-require-form-method.md | 71 ++++++ lib/rules/template-require-form-method.js | 115 ++++++++++ .../lib/rules/template-require-form-method.js | 204 ++++++++++++++++++ 4 files changed, 391 insertions(+) create mode 100644 docs/rules/template-require-form-method.md create mode 100644 lib/rules/template-require-form-method.js create mode 100644 tests/lib/rules/template-require-form-method.js diff --git a/README.md b/README.md index a59e35f549..78c9033a53 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | | | [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | | | [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | | +| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | | | | [template-require-has-block-helper](docs/rules/template-require-has-block-helper.md) | require (has-block) helper usage instead of hasBlock property | | 🔧 | | | [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | | | [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | | diff --git a/docs/rules/template-require-form-method.md b/docs/rules/template-require-form-method.md new file mode 100644 index 0000000000..a200040974 --- /dev/null +++ b/docs/rules/template-require-form-method.md @@ -0,0 +1,71 @@ +# ember/template-require-form-method + + + +Require form elements to have a method attribute. + +Form elements should explicitly specify the HTTP method they use. This improves code clarity and helps catch potential issues. + +## Examples + +This rule **forbids** the following: + +```gjs + +``` + +```gjs + +``` + +This rule **allows** the following: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +## Configuration + +- `allowedMethods` (default: `['POST', 'GET', 'DIALOG']`) - Array of allowed form method values + +```js +// .eslintrc.js +module.exports = { + rules: { + 'ember/template-require-form-method': [ + 'error', + { + allowedMethods: ['POST', 'GET'], + }, + ], + }, +}; +``` + +## References + +- [HTML Spec - Form Method Attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method) diff --git a/lib/rules/template-require-form-method.js b/lib/rules/template-require-form-method.js new file mode 100644 index 0000000000..d2879f3dd9 --- /dev/null +++ b/lib/rules/template-require-form-method.js @@ -0,0 +1,115 @@ +// Form `method` attribute keywords: +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method +const VALID_FORM_METHODS = ['POST', 'GET', 'DIALOG']; + +const DEFAULT_CONFIG = { + allowedMethods: VALID_FORM_METHODS, +}; + +function parseConfig(config) { + if (config === false || config === undefined) { + return false; + } + + if (config === true) { + return DEFAULT_CONFIG; + } + + if (typeof config === 'object' && Array.isArray(config.allowedMethods)) { + const allowedMethods = config.allowedMethods.map((m) => String(m).toUpperCase()); + + // Check if all methods are valid + const hasAllValid = allowedMethods.every((m) => VALID_FORM_METHODS.includes(m)); + + if (hasAllValid) { + return { allowedMethods }; + } + } + + return false; +} + +function makeErrorMessage(methods) { + return `All \`
\` elements should have \`method\` attribute with value of \`${methods.join(',')}\``; +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'require form method attribute', + category: 'Best Practices', + recommended: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-form-method.md', + templateMode: 'both', + }, + fixable: null, + schema: [ + { + oneOf: [ + { + type: 'object', + properties: { + allowedMethods: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + }, + ], + messages: {}, + originallyFrom: { + name: 'ember-template-lint', + rule: 'lib/rules/require-form-method.js', + docs: 'docs/rule/require-form-method.md', + tests: 'test/unit/rules/require-form-method-test.js', + }, + }, + + create(context) { + // If no options provided, use defaults + let config = context.options[0]; + config = config ? parseConfig(config) : DEFAULT_CONFIG; + + if (config === false) { + return {}; + } + + return { + GlimmerElementNode(node) { + if (node.tag !== 'form') { + return; + } + + const methodAttribute = node.attributes.find((attr) => attr.name === 'method'); + + if (!methodAttribute) { + context.report({ + node, + message: makeErrorMessage(config.allowedMethods), + }); + return; + } + + // Check if it's a text value + if (methodAttribute.value && methodAttribute.value.type === 'GlimmerTextNode') { + const methodValue = methodAttribute.value.chars.toUpperCase(); + + if (!config.allowedMethods.includes(methodValue)) { + context.report({ + node, + message: makeErrorMessage(config.allowedMethods), + }); + } + } + // If it's a dynamic value (like {{foo}}), don't report + }, + }; + }, +}; diff --git a/tests/lib/rules/template-require-form-method.js b/tests/lib/rules/template-require-form-method.js new file mode 100644 index 0000000000..789800b801 --- /dev/null +++ b/tests/lib/rules/template-require-form-method.js @@ -0,0 +1,204 @@ +const rule = require('../../../lib/rules/template-require-form-method'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('template-require-form-method', rule, { + valid: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + { + code: '', + output: null, + options: [{ allowedMethods: ['get'] }], + }, + + '', + '', + ], + invalid: [ + { + code: '', + output: null, + errors: [ + { + message: + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '', + output: null, + options: [{ allowedMethods: ['GET'] }], + errors: [ + { + message: 'All `` elements should have `method` attribute with value of `GET`', + }, + ], + }, + { + code: '', + output: null, + options: [{ allowedMethods: ['POST'] }], + errors: [ + { + message: 'All `` elements should have `method` attribute with value of `POST`', + }, + ], + }, + + { + code: '', + output: null, + errors: [ + { + message: + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '', + output: null, + errors: [ + { + message: + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '', + output: null, + errors: [ + { + message: + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '', + output: null, + errors: [ + { + message: + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-require-form-method', rule, { + valid: [ + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + // Config: allowedMethods + { + code: '
', + options: [{ allowedMethods: ['get'] }], + }, + ], + invalid: [ + { + code: '
', + output: null, + errors: [ + { + message: + 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '
', + output: null, + errors: [ + { + message: + 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '
', + output: null, + errors: [ + { + message: + 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '
', + output: null, + errors: [ + { + message: + 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + { + code: '
', + output: null, + errors: [ + { + message: + 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', + }, + ], + }, + // Config: allowedMethods + { + code: '
', + output: null, + options: [{ allowedMethods: ['get'] }], + errors: [ + { + message: 'All `
` elements should have `method` attribute with value of `GET`', + }, + ], + }, + { + code: '
', + output: null, + options: [{ allowedMethods: ['POST'] }], + errors: [ + { + message: 'All `
` elements should have `method` attribute with value of `POST`', + }, + ], + }, + ], +}); From 1ebfdf1f69e8f9475e108a8de2b7f229535e2084 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:24:51 -0400 Subject: [PATCH 2/2] Sync with template-lint --- README.md | 2 +- docs/rules/template-require-form-method.md | 70 ++--- lib/rules/template-require-form-method.js | 33 ++- .../lib/rules/template-require-form-method.js | 268 ++++++------------ 4 files changed, 139 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index 78c9033a53..b4c08414ab 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | | | [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | | | [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | | -| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | | | +| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | 🔧 | | | [template-require-has-block-helper](docs/rules/template-require-has-block-helper.md) | require (has-block) helper usage instead of hasBlock property | | 🔧 | | | [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | | | [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | | diff --git a/docs/rules/template-require-form-method.md b/docs/rules/template-require-form-method.md index a200040974..7eb3d987aa 100644 --- a/docs/rules/template-require-form-method.md +++ b/docs/rules/template-require-form-method.md @@ -1,71 +1,55 @@ # ember/template-require-form-method +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + -Require form elements to have a method attribute. +This rule requires all `` elements to have `method` attribute with `POST`, `GET` or `DIALOG` value. -Form elements should explicitly specify the HTTP method they use. This improves code clarity and helps catch potential issues. +By default `form` elements without `method` attribute are submitted as `GET` requests. +In usual applications `submit` event listeners are attached to `form` elements and `event.preventDefault()` is called to avoid form submission. -## Examples +However in case of failure to prevent default action, form submission as `GET` request can leak sensitive end-user information. -This rule **forbids** the following: +Example uses of `GET` requests: -```gjs - -``` +- non-secure data +- bookmarking the submission result +- data search query strings -```gjs - -``` +**Caution** - this rules does not check for `formmethod` attribute on `form` elements themselves. -This rule **allows** the following: +## Examples -```gjs - -``` +This rule **forbids** the following: ```gjs ``` -```gjs - -``` +This rule **allows** the following: ```gjs ``` ## Configuration -- `allowedMethods` (default: `['POST', 'GET', 'DIALOG']`) - Array of allowed form method values - -```js -// .eslintrc.js -module.exports = { - rules: { - 'ember/template-require-form-method': [ - 'error', - { - allowedMethods: ['POST', 'GET'], - }, - ], - }, -}; -``` +The following values are valid configuration: + +- boolean - `true` to enable / `false` to disable +- object -- An object with the following keys: + - `allowedMethods` -- An array of allowed form `method` attribute values, default: `['POST', 'GET', 'DIALOG']` ## References -- [HTML Spec - Form Method Attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method) +- [MDN - form method attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method) +- [HTML spec - form method attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method) diff --git a/lib/rules/template-require-form-method.js b/lib/rules/template-require-form-method.js index d2879f3dd9..66be830ca5 100644 --- a/lib/rules/template-require-form-method.js +++ b/lib/rules/template-require-form-method.js @@ -33,6 +33,10 @@ function makeErrorMessage(methods) { return `All \`\` elements should have \`method\` attribute with value of \`${methods.join(',')}\``; } +function getFixedMethod(config) { + return config.allowedMethods[0]; +} + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -40,11 +44,10 @@ module.exports = { docs: { description: 'require form method attribute', category: 'Best Practices', - recommended: false, url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-form-method.md', templateMode: 'both', }, - fixable: null, + fixable: 'code', schema: [ { oneOf: [ @@ -63,7 +66,9 @@ module.exports = { ], }, ], - messages: {}, + messages: { + invalidMethod: '{{message}}', + }, originallyFrom: { name: 'ember-template-lint', rule: 'lib/rules/require-form-method.js', @@ -92,7 +97,16 @@ module.exports = { if (!methodAttribute) { context.report({ node, - message: makeErrorMessage(config.allowedMethods), + messageId: 'invalidMethod', + data: { + message: makeErrorMessage(config.allowedMethods), + }, + fix(fixer) { + return fixer.insertTextAfterRange( + [node.parts.at(-1).range[1], node.parts.at(-1).range[1]], + ` method="${getFixedMethod(config)}"` + ); + }, }); return; } @@ -104,7 +118,16 @@ module.exports = { if (!config.allowedMethods.includes(methodValue)) { context.report({ node, - message: makeErrorMessage(config.allowedMethods), + messageId: 'invalidMethod', + data: { + message: makeErrorMessage(config.allowedMethods), + }, + fix(fixer) { + return fixer.replaceTextRange( + methodAttribute.value.range, + `"${getFixedMethod(config)}"` + ); + }, }); } } diff --git a/tests/lib/rules/template-require-form-method.js b/tests/lib/rules/template-require-form-method.js index 789800b801..5e3573b565 100644 --- a/tests/lib/rules/template-require-form-method.js +++ b/tests/lib/rules/template-require-form-method.js @@ -1,104 +1,91 @@ const rule = require('../../../lib/rules/template-require-form-method'); const RuleTester = require('eslint').RuleTester; -const ruleTester = new RuleTester({ +const DEFAULT_ERROR = + 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`'; + +const validHbs = [ + { + options: [{ allowedMethods: ['get'] }], + code: '
', + }, + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', +]; + +const invalidHbs = [ + { + options: [{ allowedMethods: ['get'] }], + code: '
', + output: '
', + errors: [ + { message: 'All `
` elements should have `method` attribute with value of `GET`' }, + ], + }, + { + options: [{ allowedMethods: ['POST'] }], + code: '
', + output: '
', + errors: [ + { message: 'All `
` elements should have `method` attribute with value of `POST`' }, + ], + }, + { + code: '
', + output: '
', + errors: [{ message: DEFAULT_ERROR }], + }, + { + code: '
', + output: '
', + errors: [{ message: DEFAULT_ERROR }], + }, + { + code: '
', + output: '
', + errors: [{ message: DEFAULT_ERROR }], + }, + { + code: '
', + output: '
', + errors: [{ message: DEFAULT_ERROR }], + }, + { + code: '
', + output: '
', + errors: [{ message: DEFAULT_ERROR }], + }, +]; + +function wrapTemplate(entry) { + if (typeof entry === 'string') { + return ``; + } + + return { + ...entry, + code: ``, + output: entry.output ? `` : entry.output, + }; +} + +const gjsRuleTester = new RuleTester({ parser: require.resolve('ember-eslint-parser'), parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, }); -ruleTester.run('template-require-form-method', rule, { - valid: [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - { - code: '', - output: null, - options: [{ allowedMethods: ['get'] }], - }, - - '', - '', - ], - invalid: [ - { - code: '', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '', - output: null, - options: [{ allowedMethods: ['GET'] }], - errors: [ - { - message: 'All `` elements should have `method` attribute with value of `GET`', - }, - ], - }, - { - code: '', - output: null, - options: [{ allowedMethods: ['POST'] }], - errors: [ - { - message: 'All `` elements should have `method` attribute with value of `POST`', - }, - ], - }, - - { - code: '', - output: null, - errors: [ - { - message: - 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '', - output: null, - errors: [ - { - message: - 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '', - output: null, - errors: [ - { - message: - 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '', - output: null, - errors: [ - { - message: - 'All `` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - ], +gjsRuleTester.run('template-require-form-method', rule, { + valid: validHbs.map(wrapTemplate), + invalid: invalidHbs.map(wrapTemplate), }); const hbsRuleTester = new RuleTester({ @@ -110,95 +97,6 @@ const hbsRuleTester = new RuleTester({ }); hbsRuleTester.run('template-require-form-method', rule, { - valid: [ - '
', - '
', - '
', - '
', - '
', - '
', - '
', - '
', - '
', - '
', - '
', - // Config: allowedMethods - { - code: '
', - options: [{ allowedMethods: ['get'] }], - }, - ], - invalid: [ - { - code: '
', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '
', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '
', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '
', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - { - code: '
', - output: null, - errors: [ - { - message: - 'All `
` elements should have `method` attribute with value of `POST,GET,DIALOG`', - }, - ], - }, - // Config: allowedMethods - { - code: '
', - output: null, - options: [{ allowedMethods: ['get'] }], - errors: [ - { - message: 'All `
` elements should have `method` attribute with value of `GET`', - }, - ], - }, - { - code: '
', - output: null, - options: [{ allowedMethods: ['POST'] }], - errors: [ - { - message: 'All `
` elements should have `method` attribute with value of `POST`', - }, - ], - }, - ], + valid: validHbs, + invalid: invalidHbs, });