diff --git a/README.md b/README.md index 4afd8f124e..7829714bad 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [template-linebreak-style](docs/rules/template-linebreak-style.md) | enforce consistent linebreaks in templates | | 🔧 | | | [template-modifier-name-case](docs/rules/template-modifier-name-case.md) | require dasherized names for modifiers | | 🔧 | | | [template-no-only-default-slot](docs/rules/template-no-only-default-slot.md) | disallow using only the default slot | | 🔧 | | +| [template-template-length](docs/rules/template-template-length.md) | enforce template size constraints | | | | ### Testing diff --git a/docs/rules/template-template-length.md b/docs/rules/template-template-length.md new file mode 100644 index 0000000000..0900eb784b --- /dev/null +++ b/docs/rules/template-template-length.md @@ -0,0 +1,57 @@ +# ember/template-template-length + + + +Enforce template size constraints. + +Very long templates can indicate that markup should be split into smaller components. +Very short templates can indicate that markup might be better inlined. + +## Config + +This rule accepts either: + +- `true` / `false` +- an object with: + - `max` (integer): maximum allowed template length in lines + - `min` (integer): minimum allowed template length in lines + +Using `true` defaults to: + +- `max: 200` +- `min: 5` + +## Examples + +Examples of **incorrect** code for this rule: + +```gjs + +``` + +with config: + +```json +{ "min": 10 } +``` + +Examples of **correct** code for this rule: + +```gjs + +``` + +## Related rules + +- [eslint/max-lines](https://eslint.org/docs/rules/max-lines) + +## References + +- [eslint-plugin-ember template-length](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-template-length.md) +- [eslint/max-lines](https://eslint.org/docs/latest/rules/max-lines) diff --git a/lib/rules/template-template-length.js b/lib/rules/template-template-length.js new file mode 100644 index 0000000000..aa14ba729b --- /dev/null +++ b/lib/rules/template-template-length.js @@ -0,0 +1,119 @@ +const DEFAULT_MAX_LENGTH = 200; +const DEFAULT_MIN_LENGTH = 5; + +const DEFAULT_CONFIG = { + max: DEFAULT_MAX_LENGTH, + min: DEFAULT_MIN_LENGTH, +}; + +function isValidConfigObjectFormat(config) { + for (const key of Object.keys(config)) { + const value = config[key]; + + if (key !== 'min' && key !== 'max') { + return false; + } + + if (!Number.isInteger(value)) { + return false; + } + } + + return true; +} + +function parseConfig(config) { + const configType = typeof config; + + switch (configType) { + case 'boolean': { + return config ? DEFAULT_CONFIG : {}; + } + case 'object': { + if (config === null) { + break; + } + + if (isValidConfigObjectFormat(config)) { + return config; + } + break; + } + case 'undefined': { + return {}; + } + // No default + } + + throw new Error( + 'The ember/template-template-length rule accepts: boolean, or object with integer "min" and/or "max" keys.' + ); +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce template size constraints', + category: 'Stylistic Issues', + recommendedGjs: false, + recommendedGts: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-template-length.md', + templateMode: 'both', + }, + schema: [ + { + oneOf: [ + { type: 'boolean' }, + { + type: 'object', + properties: { + min: { type: 'integer' }, + max: { type: 'integer' }, + }, + additionalProperties: false, + }, + ], + }, + ], + messages: { + tooLong: 'Template length of {{length}} exceeds {{max}}', + tooShort: 'Template length of {{length}} is smaller than {{min}}', + }, + originallyFrom: { + name: 'ember-template-lint', + rule: 'lib/rules/template-length.js', + docs: 'docs/rule/template-length.md', + tests: 'test/unit/rules/template-length-test.js', + }, + }, + + create(context) { + const config = parseConfig(context.options[0]); + + if (!config.min && !config.max) { + return {}; + } + + return { + 'GlimmerTemplate:exit'(node) { + const length = node.loc.end.line - node.loc.start.line + 1; + + if (config.max && length > config.max) { + context.report({ + node, + messageId: 'tooLong', + data: { length, max: config.max }, + }); + } else if (config.min && length < config.min) { + context.report({ + node, + messageId: 'tooShort', + data: { length, min: config.min }, + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/template-template-length.js b/tests/lib/rules/template-template-length.js new file mode 100644 index 0000000000..c9faaed09e --- /dev/null +++ b/tests/lib/rules/template-template-length.js @@ -0,0 +1,116 @@ +const rule = require('../../../lib/rules/template-template-length'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('template-template-length', rule, { + valid: [ + { + code: ``, + options: [{ max: 5 }], + }, + { + code: ``, + options: [{ min: 3 }], + }, + { + code: ``, + options: [false], + }, + + ``, + ], + invalid: [ + { + code: ``, + output: null, + options: [{ min: 10 }], + errors: [{ message: 'Template length of 4 is smaller than 10' }], + }, + { + code: ``, + output: null, + options: [{ max: 3 }], + errors: [{ message: 'Template length of 5 exceeds 3' }], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-template-length', rule, { + valid: [ + `testing this +and +this +and his`, + `testing +this +`, + `testing +this +and his +`, + `testing +this +andthis +`, + // Config: max option + { + code: 'testing\nthis\n', + options: [{ max: 10 }], + }, + // Config: min option + { + code: 'testing\nthis\nand\this\n', + options: [{ min: 1 }], + }, + // Config: min + max options + { + code: 'testing\nthis\nandthis\n', + options: [{ min: 1, max: 5 }], + }, + ], + invalid: [ + { + code: 'testing\ntoo-short template\n', + output: null, + options: [{ min: 10 }], + errors: [{ message: 'Template length of 3 is smaller than 10' }], + }, + { + code: 'test\nthis\nand\nthis\n', + output: null, + options: [{ max: 3 }], + errors: [{ message: 'Template length of 5 exceeds 3' }], + }, + ], +});