Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add strictSnakeCase option to rule attribute-names #207

Merged
merged 6 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion docs/rules/attribute-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 snake-case attributes.
Further, camelCase names should ideally be exposed as kebab-case attributes.

## Rule Details

Expand Down Expand Up @@ -33,6 +33,56 @@ The following patterns are not warnings:
@property({attribute: 'camel-case-name'})
camelCaseName: string;

@property({attribute: 'camel-case-other-name'})
43081j marked this conversation as resolved.
Show resolved Hide resolved
camelCaseName: string;

@property()
lower: string;
```

## Options

### `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:

| 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:

```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'})
43081j marked this conversation as resolved.
Show resolved Hide resolved
camelCaseName: string;
```

The following patterns are not warnings with `{"convention": "kebab"}`
specified:

```ts
@property({attribute: 'camel-case-name'})
camelCaseName: string;

@property({attribute: false})
camelCaseName: string;

@property()
lower: string;
```
Expand Down
48 changes: 45 additions & 3 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, toKebabCase, toSnakeCase} from '../util';

//------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -18,19 +18,32 @@ 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: {
convention: {type: 'string', enum: ['none', 'kebab', 'snake']}
},
additionalProperties: false
}
],
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)',
casedAttributeConvention:
'Attribute should be property name written in {{convention}} ' +
'as "{{name}}"'
}
},

create(context): Rule.RuleListener {
const convention = context.options[0]?.convention ?? 'none';

return {
ClassDeclaration: (node: ESTree.Class): void => {
if (isLitClass(node)) {
Expand All @@ -57,6 +70,35 @@ const rule: Rule.RuleModule = {
node: propConfig.expr ?? propConfig.key,
messageId: 'casedAttribute'
});
} else if (convention !== 'none') {
let conventionName;
let expectedAttributeName;

switch (convention) {
case 'snake':
conventionName = 'snake_case';
expectedAttributeName = toSnakeCase(prop);
break;
case 'kebab':
conventionName = 'kebab-case';
expectedAttributeName = toKebabCase(prop);
break;
}

if (
expectedAttributeName &&
conventionName &&
propConfig.attributeName !== expectedAttributeName
) {
context.report({
node: propConfig.expr ?? propConfig.key,
messageId: 'casedAttributeConvention',
data: {
convention: conventionName,
name: expectedAttributeName
}
});
}
}
}
}
Expand Down
191 changes: 191 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,76 @@ ruleTester.run('attribute-names', rule, {
};
}
}`,
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'camel-case'}
};
}
}`,
options: [{convention: 'kebab'}]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: false}
};
}
}`,
options: [{convention: 'kebab'}]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'camelcase'}
};
}
}`,
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() {
return {
camelCase: {type: String, attribute: false}
};
}
}`,
options: [{convention: 'none'}]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'camel_case'}
};
}
}`,
options: [{convention: 'snake'}]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: false}
};
}
}`,
options: [{convention: 'snake'}]
},
{
code: `class Foo extends LitElement {
@property({ type: String })
Expand Down Expand Up @@ -95,6 +165,127 @@ ruleTester.run('attribute-names', rule, {
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String}
};
}
}`,
options: [{convention: 'kebab'}],
errors: [
{
line: 4,
column: 13,
messageId: 'casedPropertyWithoutAttribute'
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'stillCamelCase'}
};
}
}`,
options: [{convention: 'kebab'}],
errors: [
{
line: 4,
column: 24,
messageId: 'casedAttribute'
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'wrong-name'}
};
}
}`,
options: [{convention: 'kebab'}],
errors: [
{
line: 4,
column: 24,
messageId: 'casedAttributeConvention',
data: {convention: 'kebab-case', name: 'camel-case'}
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String}
};
}
}`,
options: [{convention: 'none'}],
errors: [
{
line: 4,
column: 13,
messageId: 'casedPropertyWithoutAttribute'
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'camelCase'}
};
}
}`,
options: [{convention: 'none'}],
errors: [
{
line: 4,
column: 24,
messageId: 'casedAttribute'
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String}
};
}
}`,
options: [{convention: 'snake'}],
errors: [
{
line: 4,
column: 13,
messageId: 'casedPropertyWithoutAttribute'
}
]
},
{
code: `class Foo extends LitElement {
static get properties() {
return {
camelCase: {type: String, attribute: 'wrong-name'}
};
}
}`,
options: [{convention: 'snake'}],
errors: [
{
line: 4,
column: 24,
messageId: 'casedAttributeConvention',
data: {convention: 'snake_case', name: 'camel_case'}
}
]
},
{
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 kebab-case string
*
* @param {string} camelCaseStr String to convert
* @return {string}
*/
export function toKebabCase(camelCaseStr: string): string {
return camelCaseStr.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
}
Loading