Skip to content

Commit

Permalink
feat (attribute-names): add style option
Browse files Browse the repository at this point in the history
  • Loading branch information
jpradelle committed Jul 30, 2024
1 parent 406b902 commit 2006a29
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 12 deletions.
54 changes: 53 additions & 1 deletion docs/rules/attribute-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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;
```
Expand Down
63 changes: 52 additions & 11 deletions src/rules/attribute-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,19 +18,37 @@ 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 ' +
'defined in lower case',
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)) {
Expand All @@ -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'
});
}
}
}
}
Expand Down
179 changes: 179 additions & 0 deletions src/test/rules/attribute-names_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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() {
Expand Down
20 changes: 20 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

0 comments on commit 2006a29

Please sign in to comment.