From 2006a29a61480527346f9a6755bb88440d62f7f1 Mon Sep 17 00:00:00 2001 From: jpradelle Date: Tue, 23 Jul 2024 11:22:28 +0200 Subject: [PATCH] feat (attribute-names): add style option --- docs/rules/attribute-names.md | 54 +++++++- src/rules/attribute-names.ts | 63 +++++++-- src/test/rules/attribute-names_test.ts | 179 +++++++++++++++++++++++++ src/util.ts | 20 +++ 4 files changed, 304 insertions(+), 12 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index f65e4c3..223a1a2 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -4,7 +4,10 @@ Attributes are always treated lowercase, but it is common to have camelCase property names. In these situations, an explicit lowercase attribute should be supplied. -Further, camelCase names should ideally be exposed as snake-case attributes. +Further, camelCase names should ideally be exposed as dash-case attributes. + +If you want to force attribute to be exact styled version of property, +consider using `style` option. ## Rule Details @@ -33,6 +36,55 @@ The following patterns are not warnings: @property({attribute: 'camel-case-name'}) camelCaseName: string; +@property({attribute: 'camel-case-other-name'}) +camelCaseName: string; + +@property() +lower: string; +``` + +## Options + +You can specify `style` to one of these values `none`, `snake`, `dash` to +enforce that attribute name is the styled version of property, or `false`. + +For example for a property named `camelCaseProp`, expected attributes names are: + +| Style | Attribute | +|-------|-----------------| +| none | camelcaseprop | +| dash | camel-case-prop | +| snake | camel_case_prop | + +The following patterns are considered warnings with `{"style": "dash"}` +specified: + +```ts +// Using decorators: + +@property() camelCaseName: string; + +@property({attribute: 'camel-case-other-name'}) +camelCaseName: string; + +// Using a getter: + +static get properties() { + return { + camelCaseName2: {type: String} + }; +} +``` + +The following patterns are not warnings `{"strictSnakeCase": true}` specified: + +```ts +@property({attribute: 'camel-case-name'}) +camelCaseName: string; + +@property({attribute: false}) +camelCaseName: string; + @property() lower: string; ``` diff --git a/src/rules/attribute-names.ts b/src/rules/attribute-names.ts index 70277d1..b913ccf 100644 --- a/src/rules/attribute-names.ts +++ b/src/rules/attribute-names.ts @@ -5,7 +5,7 @@ import {Rule} from 'eslint'; import * as ESTree from 'estree'; -import {getPropertyMap, isLitClass} from '../util'; +import {getPropertyMap, isLitClass, toDashCase, toSnakeCase} from '../util'; //------------------------------------------------------------------------------ // Rule Definition @@ -18,7 +18,15 @@ const rule: Rule.RuleModule = { recommended: true, url: 'https://github.com/43081j/eslint-plugin-lit/blob/master/docs/rules/attribute-names.md' }, - schema: [], + schema: [ + { + type: 'object', + properties: { + style: {type: 'string', enum: ['none', 'dash', 'snake']} + }, + additionalProperties: false, + minProperties: 1 + }], messages: { casedAttribute: 'Attributes are case-insensitive and therefore should be ' + @@ -26,11 +34,21 @@ const rule: Rule.RuleModule = { casedPropertyWithoutAttribute: 'Property has non-lowercase casing but no attribute. It should ' + 'instead have an explicit `attribute` set to the lower case ' + - 'name (usually snake-case)' + 'name (usually snake-case)', + casedPropertyStyleNone: + 'Attributes should be defined with lower cased property name', + casedPropertyStyleSnake: + 'Attributes should be defined with snake_cased property name', + casedPropertyStyleDash: + 'Attributes should be defined with dash-cased property name' } }, create(context): Rule.RuleListener { + const style: string = context.options.length && context.options[0].style + ? context.options[0].style + : null; + return { ClassDeclaration: (node: ESTree.Class): void => { if (isLitClass(node)) { @@ -49,14 +67,37 @@ const rule: Rule.RuleModule = { }); } } else { - if ( - propConfig.attributeName.toLowerCase() !== - propConfig.attributeName - ) { - context.report({ - node: propConfig.expr ?? propConfig.key, - messageId: 'casedAttribute' - }); + if (style === 'none') { + if (propConfig.attributeName !== prop.toLowerCase()) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedPropertyStyleNone' + }); + } + } else if (style === 'snake') { + if (propConfig.attributeName !== toSnakeCase(prop)) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedPropertyStyleSnake' + }); + } + } else if (style === 'dash') { + if (propConfig.attributeName !== toDashCase(prop)) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedPropertyStyleDash' + }); + } + } else if (style === null) { + if ( + propConfig.attributeName.toLowerCase() !== + propConfig.attributeName + ) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedAttribute' + }); + } } } } diff --git a/src/test/rules/attribute-names_test.ts b/src/test/rules/attribute-names_test.ts index 80ee3f3..7c0f484 100644 --- a/src/test/rules/attribute-names_test.ts +++ b/src/test/rules/attribute-names_test.ts @@ -52,6 +52,66 @@ ruleTester.run('attribute-names', rule, { }; } }`, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camel-case'} + }; + } + }`, + options: [{style: 'dash'}] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: false} + }; + } + }`, + options: [{style: 'dash'}] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camelcase'} + }; + } + }`, + options: [{style: 'none'}] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: false} + }; + } + }`, + options: [{style: 'none'}] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camel_case'} + }; + } + }`, + options: [{style: 'snake'}] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: false} + }; + } + }`, + options: [{style: 'snake'}] + }, { code: `class Foo extends LitElement { @property({ type: String }) @@ -95,6 +155,125 @@ ruleTester.run('attribute-names', rule, { } ] }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String} + }; + } + }`, + options: [{style: 'dash'}], + errors: [ + { + line: 4, + column: 13, + messageId: 'casedPropertyWithoutAttribute' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'stillCamelCase'} + }; + } + }`, + options: [{style: 'dash'}], + errors: [ + { + line: 4, + column: 24, + messageId: 'casedPropertyStyleDash' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camel-Case'} + }; + } + }`, + options: [{style: 'dash'}], + errors: [ + { + line: 4, + column: 24, + messageId: 'casedPropertyStyleDash' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String} + }; + } + }`, + options: [{style: 'none'}], + errors: [ + { + line: 4, + column: 13, + messageId: 'casedPropertyWithoutAttribute' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camelCase'} + }; + } + }`, + options: [{style: 'none'}], + errors: [ + { + line: 4, + column: 24, + messageId: 'casedPropertyStyleNone' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String} + }; + } + }`, + options: [{style: 'snake'}], + errors: [ + { + line: 4, + column: 13, + messageId: 'casedPropertyWithoutAttribute' + } + ] + }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camel_Case'} + }; + } + }`, + options: [{style: 'snake'}], + errors: [ + { + line: 4, + column: 24, + messageId: 'casedPropertyStyleSnake' + } + ] + }, { code: `class Foo extends LitElement { static get properties() { diff --git a/src/util.ts b/src/util.ts index 88e81c8..face3ca 100644 --- a/src/util.ts +++ b/src/util.ts @@ -327,3 +327,23 @@ export function templateExpressionToHtml( return html; } + +/** + * Converts a camelCase string to snake_case string + * + * @param {string} camelCaseStr String to convert + * @return {string} + */ +export function toSnakeCase(camelCaseStr: string): string { + return camelCaseStr.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase()); +} + +/** + * Converts a camelCase string to dash-case string + * + * @param {string} camelCaseStr String to convert + * @return {string} + */ +export function toDashCase(camelCaseStr: string): string { + return camelCaseStr.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); +}