From 367f5cecb8f6c11cf2ef0d779c697a92893cd32b Mon Sep 17 00:00:00 2001 From: jpradelle Date: Tue, 23 Jul 2024 11:22:28 +0200 Subject: [PATCH 1/6] 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..2073e03 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 attribute 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 `{"style": "dash"}` 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()); +} From c8b7b3aca18eea891318ff839a8cf02f73869755 Mon Sep 17 00:00:00 2001 From: jpradelle Date: Tue, 30 Jul 2024 11:35:17 +0200 Subject: [PATCH 2/6] fix PR #207 --- docs/rules/attribute-names.md | 20 +++++--------------- src/rules/attribute-names.ts | 14 +++++++------- src/test/rules/attribute-names_test.ts | 14 +++++++------- src/util.ts | 4 ++-- 4 files changed, 21 insertions(+), 31 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index 2073e03..d210c1e 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -4,7 +4,7 @@ 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 dash-case attributes. +Further, camelCase names should ideally be exposed as kebab-case attributes. If you want to force attribute to be exact styled version of property, consider using `style` option. @@ -45,7 +45,7 @@ lower: string; ## Options -You can specify `style` to one of these values `none`, `snake`, `dash` to +You can specify `style` to one of these values `none`, `snake`, `kebab` to enforce that attribute name is the styled version of property, or `false`. For example for a property named `camelCaseProp`, expected attribute names are: @@ -53,30 +53,20 @@ For example for a property named `camelCaseProp`, expected attribute names are: | Style | Attribute | |-------|-----------------| | none | camelcaseprop | -| dash | camel-case-prop | +| kebab | camel-case-prop | | snake | camel_case_prop | -The following patterns are considered warnings with `{"style": "dash"}` +The following patterns are considered warnings with `{"style": "kebab"}` 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 `{"style": "dash"}` specified: +The following patterns are not warnings with `{"style": "kebab"}` specified: ```ts @property({attribute: 'camel-case-name'}) diff --git a/src/rules/attribute-names.ts b/src/rules/attribute-names.ts index b913ccf..20a9213 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, toDashCase, toSnakeCase} from '../util'; +import {getPropertyMap, isLitClass, toKebabCase, toSnakeCase} from '../util'; //------------------------------------------------------------------------------ // Rule Definition @@ -22,7 +22,7 @@ const rule: Rule.RuleModule = { { type: 'object', properties: { - style: {type: 'string', enum: ['none', 'dash', 'snake']} + style: {type: 'string', enum: ['none', 'kebab', 'snake']} }, additionalProperties: false, minProperties: 1 @@ -39,8 +39,8 @@ const rule: Rule.RuleModule = { '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' + casedPropertyStyleKebab: + 'Attributes should be defined with kebab-cased property name' } }, @@ -81,11 +81,11 @@ const rule: Rule.RuleModule = { messageId: 'casedPropertyStyleSnake' }); } - } else if (style === 'dash') { - if (propConfig.attributeName !== toDashCase(prop)) { + } else if (style === 'kebab') { + if (propConfig.attributeName !== toKebabCase(prop)) { context.report({ node: propConfig.expr ?? propConfig.key, - messageId: 'casedPropertyStyleDash' + messageId: 'casedPropertyStyleKebab' }); } } else if (style === null) { diff --git a/src/test/rules/attribute-names_test.ts b/src/test/rules/attribute-names_test.ts index 7c0f484..6dae737 100644 --- a/src/test/rules/attribute-names_test.ts +++ b/src/test/rules/attribute-names_test.ts @@ -60,7 +60,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'dash'}] + options: [{style: 'kebab'}] }, { code: `class Foo extends LitElement { @@ -70,7 +70,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'dash'}] + options: [{style: 'kebab'}] }, { code: `class Foo extends LitElement { @@ -163,7 +163,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'dash'}], + options: [{style: 'kebab'}], errors: [ { line: 4, @@ -180,12 +180,12 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'dash'}], + options: [{style: 'kebab'}], errors: [ { line: 4, column: 24, - messageId: 'casedPropertyStyleDash' + messageId: 'casedPropertyStyleKebab' } ] }, @@ -197,12 +197,12 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'dash'}], + options: [{style: 'kebab'}], errors: [ { line: 4, column: 24, - messageId: 'casedPropertyStyleDash' + messageId: 'casedPropertyStyleKebab' } ] }, diff --git a/src/util.ts b/src/util.ts index face3ca..65527e9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -339,11 +339,11 @@ export function toSnakeCase(camelCaseStr: string): string { } /** - * Converts a camelCase string to dash-case string + * Converts a camelCase string to kebab-case string * * @param {string} camelCaseStr String to convert * @return {string} */ -export function toDashCase(camelCaseStr: string): string { +export function toKebabCase(camelCaseStr: string): string { return camelCaseStr.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); } From 69ec92fcf1812cccc17464b1da1db9e991f141a1 Mon Sep 17 00:00:00 2001 From: jpradelle Date: Tue, 30 Jul 2024 14:43:31 +0200 Subject: [PATCH 3/6] fix (attribute-names) PR #207: remove duplicated code --- docs/rules/attribute-names.md | 2 +- src/rules/attribute-names.ts | 58 +++++++++++++------------- src/test/rules/attribute-names_test.ts | 12 ++++-- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index d210c1e..0c55aa8 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -46,7 +46,7 @@ lower: string; ## Options You can specify `style` to one of these values `none`, `snake`, `kebab` to -enforce that attribute name is the styled version of property, or `false`. +enforce that the attribute name is cased using the specified casing style. For example for a property named `camelCaseProp`, expected attribute names are: diff --git a/src/rules/attribute-names.ts b/src/rules/attribute-names.ts index 20a9213..32dcb20 100644 --- a/src/rules/attribute-names.ts +++ b/src/rules/attribute-names.ts @@ -35,12 +35,9 @@ const rule: Rule.RuleModule = { 'Property has non-lowercase casing but no attribute. It should ' + 'instead have an explicit `attribute` set to the lower 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', - casedPropertyStyleKebab: - 'Attributes should be defined with kebab-cased property name' + casedAttributeStyled: + 'Attributes are case-insensitive. Attributes should be written as a ' + + '{{style}} name' } }, @@ -67,28 +64,7 @@ const rule: Rule.RuleModule = { }); } } else { - 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 === 'kebab') { - if (propConfig.attributeName !== toKebabCase(prop)) { - context.report({ - node: propConfig.expr ?? propConfig.key, - messageId: 'casedPropertyStyleKebab' - }); - } - } else if (style === null) { + if (style === null) { if ( propConfig.attributeName.toLowerCase() !== propConfig.attributeName @@ -98,6 +74,32 @@ const rule: Rule.RuleModule = { messageId: 'casedAttribute' }); } + } else { + let styleName: string; + let styledKey: string; + + switch (style) { + case 'snake': + styleName = 'snake_case'; + styledKey = toSnakeCase(prop); + break; + case 'kebab': + styleName = 'kebab-case'; + styledKey = toKebabCase(prop); + break; + default: + styleName = 'lower case'; + styledKey = prop.toLowerCase(); + break; + } + + if (propConfig.attributeName !== styledKey) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedAttributeStyled', + data: {style: styleName} + }); + } } } } diff --git a/src/test/rules/attribute-names_test.ts b/src/test/rules/attribute-names_test.ts index 6dae737..1e512eb 100644 --- a/src/test/rules/attribute-names_test.ts +++ b/src/test/rules/attribute-names_test.ts @@ -185,7 +185,8 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedPropertyStyleKebab' + messageId: 'casedAttributeStyled', + data: {style: 'kebab-case'} } ] }, @@ -202,7 +203,8 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedPropertyStyleKebab' + messageId: 'casedAttributeStyled', + data: {style: 'kebab-case'} } ] }, @@ -236,7 +238,8 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedPropertyStyleNone' + messageId: 'casedAttributeStyled', + data: {style: 'lower case'} } ] }, @@ -270,7 +273,8 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedPropertyStyleSnake' + messageId: 'casedAttributeStyled', + data: {style: 'snake_case'} } ] }, From d084c47ac7f7fbc3ec410da06768dffcc4bdb0e9 Mon Sep 17 00:00:00 2001 From: jpradelle Date: Thu, 1 Aug 2024 17:04:11 +0200 Subject: [PATCH 4/6] fix (attribute-names) PR #207: rename style option to convention --- docs/rules/attribute-names.md | 19 ++++++------ src/rules/attribute-names.ts | 38 +++++++++++------------ src/test/rules/attribute-names_test.ts | 42 +++++++++++++------------- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index 0c55aa8..e125411 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -45,18 +45,18 @@ lower: string; ## Options -You can specify `style` to one of these values `none`, `snake`, `kebab` to -enforce that the attribute name is cased using the specified casing style. +You can specify `convention` to one of these values `none`, `snake`, `kebab` to +enforce that the attribute name is cased using the specified casing convention. For example for a property named `camelCaseProp`, expected attribute names are: -| Style | Attribute | -|-------|-----------------| -| none | camelcaseprop | -| kebab | camel-case-prop | -| snake | camel_case_prop | +| Convention | Attribute | +|------------|-----------------| +| none | camelcaseprop | +| kebab | camel-case-prop | +| snake | camel_case_prop | -The following patterns are considered warnings with `{"style": "kebab"}` +The following patterns are considered warnings with `{"convention": "kebab"}` specified: ```ts @@ -66,7 +66,8 @@ specified: camelCaseName: string; ``` -The following patterns are not warnings with `{"style": "kebab"}` specified: +The following patterns are not warnings with `{"convention": "kebab"}` +specified: ```ts @property({attribute: 'camel-case-name'}) diff --git a/src/rules/attribute-names.ts b/src/rules/attribute-names.ts index 32dcb20..cea396e 100644 --- a/src/rules/attribute-names.ts +++ b/src/rules/attribute-names.ts @@ -22,7 +22,7 @@ const rule: Rule.RuleModule = { { type: 'object', properties: { - style: {type: 'string', enum: ['none', 'kebab', 'snake']} + convention: {type: 'string', enum: ['none', 'kebab', 'snake']} }, additionalProperties: false, minProperties: 1 @@ -35,15 +35,15 @@ const rule: Rule.RuleModule = { 'Property has non-lowercase casing but no attribute. It should ' + 'instead have an explicit `attribute` set to the lower case ' + 'name (usually snake-case)', - casedAttributeStyled: - 'Attributes are case-insensitive. Attributes should be written as a ' + - '{{style}} name' + casedAttributeConvention: + 'Attribute should be property name written in {{convention}}' } }, create(context): Rule.RuleListener { - const style: string = context.options.length && context.options[0].style - ? context.options[0].style + const convention: string = context.options.length + && context.options[0].convention + ? context.options[0].convention : null; return { @@ -64,7 +64,7 @@ const rule: Rule.RuleModule = { }); } } else { - if (style === null) { + if (convention === null) { if ( propConfig.attributeName.toLowerCase() !== propConfig.attributeName @@ -75,29 +75,29 @@ const rule: Rule.RuleModule = { }); } } else { - let styleName: string; - let styledKey: string; + let conventionName: string; + let expectedAttributeName: string; - switch (style) { + switch (convention) { case 'snake': - styleName = 'snake_case'; - styledKey = toSnakeCase(prop); + conventionName = 'snake_case'; + expectedAttributeName = toSnakeCase(prop); break; case 'kebab': - styleName = 'kebab-case'; - styledKey = toKebabCase(prop); + conventionName = 'kebab-case'; + expectedAttributeName = toKebabCase(prop); break; default: - styleName = 'lower case'; - styledKey = prop.toLowerCase(); + conventionName = 'lower case'; + expectedAttributeName = prop.toLowerCase(); break; } - if (propConfig.attributeName !== styledKey) { + if (propConfig.attributeName !== expectedAttributeName) { context.report({ node: propConfig.expr ?? propConfig.key, - messageId: 'casedAttributeStyled', - data: {style: styleName} + messageId: 'casedAttributeConvention', + data: {convention: conventionName} }); } } diff --git a/src/test/rules/attribute-names_test.ts b/src/test/rules/attribute-names_test.ts index 1e512eb..84e8486 100644 --- a/src/test/rules/attribute-names_test.ts +++ b/src/test/rules/attribute-names_test.ts @@ -60,7 +60,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'kebab'}] + options: [{convention: 'kebab'}] }, { code: `class Foo extends LitElement { @@ -70,7 +70,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'kebab'}] + options: [{convention: 'kebab'}] }, { code: `class Foo extends LitElement { @@ -80,7 +80,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'none'}] + options: [{convention: 'none'}] }, { code: `class Foo extends LitElement { @@ -90,7 +90,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'none'}] + options: [{convention: 'none'}] }, { code: `class Foo extends LitElement { @@ -100,7 +100,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'snake'}] + options: [{convention: 'snake'}] }, { code: `class Foo extends LitElement { @@ -110,7 +110,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'snake'}] + options: [{convention: 'snake'}] }, { code: `class Foo extends LitElement { @@ -163,7 +163,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'kebab'}], + options: [{convention: 'kebab'}], errors: [ { line: 4, @@ -180,13 +180,13 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'kebab'}], + options: [{convention: 'kebab'}], errors: [ { line: 4, column: 24, - messageId: 'casedAttributeStyled', - data: {style: 'kebab-case'} + messageId: 'casedAttributeConvention', + data: {convention: 'kebab-case'} } ] }, @@ -198,13 +198,13 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'kebab'}], + options: [{convention: 'kebab'}], errors: [ { line: 4, column: 24, - messageId: 'casedAttributeStyled', - data: {style: 'kebab-case'} + messageId: 'casedAttributeConvention', + data: {convention: 'kebab-case'} } ] }, @@ -216,7 +216,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'none'}], + options: [{convention: 'none'}], errors: [ { line: 4, @@ -233,13 +233,13 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'none'}], + options: [{convention: 'none'}], errors: [ { line: 4, column: 24, - messageId: 'casedAttributeStyled', - data: {style: 'lower case'} + messageId: 'casedAttributeConvention', + data: {convention: 'lower case'} } ] }, @@ -251,7 +251,7 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'snake'}], + options: [{convention: 'snake'}], errors: [ { line: 4, @@ -268,13 +268,13 @@ ruleTester.run('attribute-names', rule, { }; } }`, - options: [{style: 'snake'}], + options: [{convention: 'snake'}], errors: [ { line: 4, column: 24, - messageId: 'casedAttributeStyled', - data: {style: 'snake_case'} + messageId: 'casedAttributeConvention', + data: {convention: 'snake_case'} } ] }, From eff039f38d9d9632dffcf30dcb02ace7fd1ad65f Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:46:33 +0100 Subject: [PATCH 5/6] feat: always warn on lowercase mismatches --- docs/rules/attribute-names.md | 17 ++++++--- src/rules/attribute-names.ts | 53 +++++++++++++------------- src/test/rules/attribute-names_test.ts | 24 ++++++++---- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index e125411..d72954b 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -6,9 +6,6 @@ be supplied. Further, camelCase names should ideally be exposed as kebab-case attributes. -If you want to force attribute to be exact styled version of property, -consider using `style` option. - ## Rule Details This rule enforces that all lit properties have equivalent lower case attributes @@ -45,8 +42,16 @@ lower: string; ## Options -You can specify `convention` to one of these values `none`, `snake`, `kebab` to -enforce that the attribute name is cased using the specified casing convention. +### `convention` + +You can specify a `convention` to enforce a particular naming convention +on element attributes. + +The available values are: + +- `none` (default, no convention is enforced) +- `kebab` +- `snake` For example for a property named `camelCaseProp`, expected attribute names are: @@ -60,8 +65,10 @@ The following patterns are considered warnings with `{"convention": "kebab"}` specified: ```ts +// Should have an attribute set to `camel-case-name` @property() camelCaseName: string; +// Attribute should match the property name when a convention is set @property({attribute: 'camel-case-other-name'}) camelCaseName: string; ``` diff --git a/src/rules/attribute-names.ts b/src/rules/attribute-names.ts index cea396e..aab2800 100644 --- a/src/rules/attribute-names.ts +++ b/src/rules/attribute-names.ts @@ -24,9 +24,9 @@ const rule: Rule.RuleModule = { properties: { convention: {type: 'string', enum: ['none', 'kebab', 'snake']} }, - additionalProperties: false, - minProperties: 1 - }], + additionalProperties: false + } + ], messages: { casedAttribute: 'Attributes are case-insensitive and therefore should be ' + @@ -36,15 +36,13 @@ const rule: Rule.RuleModule = { 'instead have an explicit `attribute` set to the lower case ' + 'name (usually snake-case)', casedAttributeConvention: - 'Attribute should be property name written in {{convention}}' + 'Attribute should be property name written in {{convention}} ' + + 'as "{{name}}"' } }, create(context): Rule.RuleListener { - const convention: string = context.options.length - && context.options[0].convention - ? context.options[0].convention - : null; + const convention = context.options[0]?.convention ?? 'none'; return { ClassDeclaration: (node: ESTree.Class): void => { @@ -64,19 +62,17 @@ const rule: Rule.RuleModule = { }); } } else { - if (convention === null) { - if ( - propConfig.attributeName.toLowerCase() !== - propConfig.attributeName - ) { - context.report({ - node: propConfig.expr ?? propConfig.key, - messageId: 'casedAttribute' - }); - } - } else { - let conventionName: string; - let expectedAttributeName: string; + if ( + propConfig.attributeName.toLowerCase() !== + propConfig.attributeName + ) { + context.report({ + node: propConfig.expr ?? propConfig.key, + messageId: 'casedAttribute' + }); + } else if (convention !== 'none') { + let conventionName; + let expectedAttributeName; switch (convention) { case 'snake': @@ -87,17 +83,20 @@ const rule: Rule.RuleModule = { conventionName = 'kebab-case'; expectedAttributeName = toKebabCase(prop); break; - default: - conventionName = 'lower case'; - expectedAttributeName = prop.toLowerCase(); - break; } - if (propConfig.attributeName !== expectedAttributeName) { + if ( + expectedAttributeName && + conventionName && + propConfig.attributeName !== expectedAttributeName + ) { context.report({ node: propConfig.expr ?? propConfig.key, messageId: 'casedAttributeConvention', - data: {convention: conventionName} + data: { + convention: conventionName, + name: expectedAttributeName + } }); } } diff --git a/src/test/rules/attribute-names_test.ts b/src/test/rules/attribute-names_test.ts index 84e8486..36fe4c3 100644 --- a/src/test/rules/attribute-names_test.ts +++ b/src/test/rules/attribute-names_test.ts @@ -82,6 +82,16 @@ ruleTester.run('attribute-names', rule, { }`, options: [{convention: 'none'}] }, + { + code: `class Foo extends LitElement { + static get properties() { + return { + camelCase: {type: String, attribute: 'camel-case'} + }; + } + }`, + options: [{convention: 'none'}] + }, { code: `class Foo extends LitElement { static get properties() { @@ -185,8 +195,7 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedAttributeConvention', - data: {convention: 'kebab-case'} + messageId: 'casedAttribute' } ] }, @@ -194,7 +203,7 @@ ruleTester.run('attribute-names', rule, { code: `class Foo extends LitElement { static get properties() { return { - camelCase: {type: String, attribute: 'camel-Case'} + camelCase: {type: String, attribute: 'wrong-name'} }; } }`, @@ -204,7 +213,7 @@ ruleTester.run('attribute-names', rule, { line: 4, column: 24, messageId: 'casedAttributeConvention', - data: {convention: 'kebab-case'} + data: {convention: 'kebab-case', name: 'camel-case'} } ] }, @@ -238,8 +247,7 @@ ruleTester.run('attribute-names', rule, { { line: 4, column: 24, - messageId: 'casedAttributeConvention', - data: {convention: 'lower case'} + messageId: 'casedAttribute' } ] }, @@ -264,7 +272,7 @@ ruleTester.run('attribute-names', rule, { code: `class Foo extends LitElement { static get properties() { return { - camelCase: {type: String, attribute: 'camel_Case'} + camelCase: {type: String, attribute: 'wrong-name'} }; } }`, @@ -274,7 +282,7 @@ ruleTester.run('attribute-names', rule, { line: 4, column: 24, messageId: 'casedAttributeConvention', - data: {convention: 'snake_case'} + data: {convention: 'snake_case', name: 'camel_case'} } ] }, From 9def30c16124bea70df4129daecb2a4cd99a143e Mon Sep 17 00:00:00 2001 From: jpradelle Date: Thu, 29 Aug 2024 13:25:17 +0200 Subject: [PATCH 6/6] fix (attribute-names) PR #207: update documentation for none convention --- docs/rules/attribute-names.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/rules/attribute-names.md b/docs/rules/attribute-names.md index d72954b..bcffeff 100644 --- a/docs/rules/attribute-names.md +++ b/docs/rules/attribute-names.md @@ -55,11 +55,11 @@ The available values are: For example for a property named `camelCaseProp`, expected attribute names are: -| Convention | Attribute | -|------------|-----------------| -| none | camelcaseprop | -| kebab | camel-case-prop | -| snake | camel_case_prop | +| Convention | Attribute | +|------------|----------------------| +| none | any lower case value | +| kebab | camel-case-prop | +| snake | camel_case_prop | The following patterns are considered warnings with `{"convention": "kebab"}` specified: