Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Sep 18, 2023. It is now read-only.

File name matches element #6

Merged
merged 3 commits into from
Mar 25, 2021
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ JSON ESLint config example:

- [Define Tag After Class Definition](./docs/rules/define-tag-after-class-definition.md)
- [Expose Class on Global](./docs/rules/expose-class-on-global.md)
- [File Name Matches Element](./docs/rules/file-name-matches-element.md)
75 changes: 75 additions & 0 deletions docs/rules/file-name-matches-element.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# File Name Matches Element

For code organisation, it is useful if a file name matches the contents of a class inside it. In this instance, having the file name match the name of the element.

## Rule Details

This rule enforces the name of the file an Element exists in matches the name of the Element itself.

👎 Examples of **incorrect** code for this rule:

```js
// elements.ts
class FooBarElement extends HTMLElement {
}
```

👍 Examples of **correct** code for this rule:

```js
// foo-bar-elements.ts
class FooBarElement extends HTMLElement {
}
```

### Options

- `transform` can define how a class name would be transformed into a file name. You can specify multiple transforms to allow multiple options. The default is `['kebab', 'pascal']`. The options are:
- `none`: Files must be named exactly as the class name: `FooBarElement` -> `FooBarElement.js`
- `snake`: Files must be named lowercase, separated by underscores: `FooBarElement` -> `foo_bar_element.js`
- `kebab`: Files must be named lowercase, separated by dashes: `FooBarElement` -> `foo-bar-element.js`
- `pascal`: Files must match case, but the first character must be lower: `FooBarElement` -> `fooBarElement.js`

- `suffix` can allow omission of a suffix from a file name. Case insensitive.
- `prefix` can allow omission of a prefix from a file name. Case insensitive.
- `matchDirectory` the directory structure of the file is also counted as a valid prefix.

👍 Examples of **correct** file names to class names:

- `["error", {"transform": "snake", "suffix": ["Element"]}]`
- `FooBarElement` -> `foo_bar.js`
- `FooBarElement` -> `foo_bar_element.js`

- `["error", {"transform": ["snake", "kebab"], "suffix": ["Element"]}]`
- `FooBarElement` -> `foo-bar.js`
- `FooBarElement` -> `foo-bar.js`
- `FooBarElement` -> `foo_bar_element.js`
- `FooBarElement` -> `foo-bar-element.js`

- `["error", {"transform": ["snake", "kebab"], "suffix": ["Element", "Component"]}]`
- `FooBarElement` -> `foo-bar.js`
- `FooBarElement` -> `foo-bar.js`
- `FooBarElement` -> `foo_bar_element.js`
- `FooBarElement` -> `foo-bar-element.js`
- `FooBarComponent` -> `foo-bar.js`
- `FooBarComponent` -> `foo-bar.js`
- `FooBarComponent` -> `foo_bar_component.js`
- `FooBarComponent` -> `foo-bar-component.js`

- `["error", {"transform": ["kebab"], "prefix": ["foo"]}]`
- `FooBarElement` -> `foo-bar-element.js`
- `FooBarElement` -> `bar-element.js`

- `["error", {"transform": ["kebab"], "matchDirectory": true}]`
- `FooBarElement` -> `foo-bar-element.js`
- `FooBarElement` -> `foo/bar-element.js`
- `FooBarElement` -> `foo/bar/element.js`
- `FooBarElement` -> `foobar/element.js`

## When Not To Use It

If you intentionally want to name your custom elements differently to the file names, then you may disable this rule.

## Version

This rule was introduced in v0.0.1
3 changes: 2 additions & 1 deletion lib/rules.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition'),
'expose-class-on-global': require('./rules/expose-class-on-global')
'expose-class-on-global': require('./rules/expose-class-on-global'),
'file-name-matches-element': require('./rules/file-name-matches-element')
}
120 changes: 120 additions & 0 deletions lib/rules/file-name-matches-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const s = require('../custom-selectors')
const {basename, dirname, extname} = require('path')
const transformFuncs = {
snake(str) {
return str
.replace(/([A-Z]($|[a-z]))/g, '_$1')
.replace(/^_/g, '')
.toLowerCase()
},
kebab(str) {
return str
.replace(/([A-Z]($|[a-z]))/g, '-$1')
.replace(/^-/g, '')
.toLowerCase()
},
pascal(str) {
return str.replace(/^./g, c => c.toLowerCase())
},
none(str) {
return str
}
}

function hasFileName(context) {
const file = context.getFilename()
return !(file === '<input>' || file === '<text>')
}

function* generateNames(prefixes, suffixes, name) {
for (const prefix of prefixes) {
if (name.toLowerCase().startsWith(prefix.toLowerCase())) {
const truncated = name.substr(prefix.length)
yield truncated
for (const suffix of suffixes) {
if (truncated.toLowerCase().endsWith(suffix.toLowerCase())) {
yield truncated.substr(0, truncated.length - suffix.length)
}
}
}
}
for (const suffix of suffixes) {
if (name.toLowerCase().endsWith(suffix.toLowerCase())) {
const truncated = name.substr(0, name.length - suffix.length)
yield truncated
for (const prefix of prefixes) {
if (truncated.toLowerCase().startsWith(prefix.toLowerCase())) {
yield truncated.substr(prefix.length)
}
}
}
}
}

function* expandDirectoryNames(path) {
const dirs = path.toLowerCase().split(/[/\\]/g)
while (dirs.length) {
yield dirs.join('')
dirs.shift()
}
}

module.exports = {
meta: {
type: 'suggestion',
docs: {description: '', url: require('../url')(module)}
},
schema: [
{
type: 'object',
properties: {
transform: {
oneOf: [
{enum: ['none', 'snake', 'kebab', 'pascal']},
{
type: 'array',
items: {enum: ['none', 'snake', 'kebab', 'pascal']},
minItems: 1,
maxItems: 4
}
]
},
suffix: {
onfOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}]
},
matchDirectory: {
type: 'boolean'
}
}
}
],
create(context) {
if (!hasFileName(context)) return {}
return {
[s.HTMLElementClass](node) {
const name = node.id.name
const names = [name]
const filename = basename(context.getFilename(), extname(context.getFilename()))
const transforms = [].concat(context.options?.[0]?.transform || ['kebab', 'pascal'])
const suffixes = [].concat(context.options?.[0]?.suffix || [])
const prefixes = [].concat(context.options?.[0]?.prefix || [])
if (context.options?.[0]?.matchDirectory) {
prefixes.push(...expandDirectoryNames(dirname(context.getFilename())))
}
for (const newName of generateNames(prefixes, suffixes, name)) {
names.push(newName)
}
const allowedFileNames = new Set()
for (const className of names) {
for (const transform of transforms) {
allowedFileNames.add(transformFuncs[transform](className))
}
}
if (!allowedFileNames.has(filename)) {
const allowed = Array.from(allowedFileNames).join('" or "')
context.report(node, `File name should be "${allowed}" but was "${filename}"`)
}
}
}
}
}
121 changes: 121 additions & 0 deletions test/file-name-matches-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const rule = require('../lib/rules/file-name-matches-element')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester({env: {es2020: true}})
const code = 'class FooBarElement extends HTMLElement {}'

ruleTester.run('file-name-matches-element', rule, {
valid: [
{code: 'class SomeMap extends Map {}', filename: 'not-an-element.js'},
{code: 'class FooBarElement {}', filename: 'not-related.js'},
{code},
{code, filename: '<text>'},
{code, filename: '<input>'},
{code, filename: 'fooBarElement.js'},
{code, filename: 'fooBarElement.ts'},
{code, filename: 'fooBarElement.jsx'},
{code, filename: 'foo-bar-element.js'},
{code, filename: 'foo-bar-element.ts'},
{code, filename: 'foo-bar-element.jsx'},
{code, filename: 'FooBarElement.js', options: [{transform: 'none'}]},
{code, filename: 'FooBarElement.ts', options: [{transform: 'none'}]},
{code, filename: 'FooBarElement.jsx', options: [{transform: 'none'}]},
{code, filename: 'foo_bar_element.js', options: [{transform: 'snake'}]},
{code, filename: 'foo_bar_element.ts', options: [{transform: 'snake'}]},
{code, filename: 'foo_bar_element.jsx', options: [{transform: 'snake'}]},
{code, filename: 'foo-bar-element.js', options: [{transform: 'kebab'}]},
{code, filename: 'foo-bar-element.ts', options: [{transform: 'kebab'}]},
{code, filename: 'foo-bar-element.jsx', options: [{transform: 'kebab'}]},
{code, filename: 'fooBarElement.js', options: [{transform: 'pascal'}]},
{code, filename: 'fooBarElement.ts', options: [{transform: 'pascal'}]},
{code, filename: 'fooBarElement.jsx', options: [{transform: 'pascal'}]},
{code, filename: 'fooBarElement.js', options: [{transform: ['snake', 'kebab', 'pascal']}]},
{code, filename: 'fooBarElement.ts', options: [{transform: ['snake', 'kebab', 'pascal']}]},
{code, filename: 'fooBarElement.jsx', options: [{transform: ['snake', 'kebab', 'pascal']}]},
{code, filename: 'fooBar.js', options: [{transform: 'pascal', suffix: 'Element'}]},
{code, filename: 'foo-bar.js', options: [{transform: 'kebab', suffix: 'element'}]},
{code, filename: 'foo_bar.js', options: [{transform: 'snake', suffix: 'element'}]},
{code, filename: 'foo_bar.js', options: [{transform: ['kebab', 'snake', 'pascal'], suffix: 'Element'}]},
{code, filename: 'foo_bar_element.js', options: [{transform: ['kebab', 'snake'], suffix: ['Element']}]},
{code, filename: 'fooBarElement.js', options: [{transform: ['kebab', 'pascal'], suffix: ['ElEmEnT']}]},
{code, filename: 'foo_bar_element.js', options: [{transform: ['kebab', 'snake'], suffix: ['Element']}]},
{code, filename: 'bar_element.js', options: [{transform: ['kebab', 'snake'], prefix: ['Foo']}]},
{code, filename: 'bar_element.js', options: [{transform: ['kebab', 'snake'], prefix: ['fOo']}]},
{
code: 'class FooBarController extends HTMLElement {}',
filename: 'components/foo/bar.ts',
options: [{transform: 'kebab', suffix: ['Controller'], matchDirectory: true}]
},
{
code: 'class FooBarController extends HTMLElement {}',
filename: 'components/foo/foo-bar.ts',
options: [{transform: 'kebab', suffix: ['Controller']}]
}
],
invalid: [
{
code,
filename: 'barfooelement.ts',
errors: [
{
message: 'File name should be "foo-bar-element" or "fooBarElement" but was "barfooelement"',
type: 'ClassDeclaration'
}
]
},
{
code,
filename: 'foobarelement.ts',
errors: [
{
message: 'File name should be "foo-bar-element" or "fooBarElement" but was "foobarelement"',
type: 'ClassDeclaration'
}
]
},
{
code,
filename: 'foobarelement.ts',
options: [{transform: ['snake']}],
errors: [
{
message: 'File name should be "foo_bar_element" but was "foobarelement"',
type: 'ClassDeclaration'
}
]
},
{
code,
filename: 'foo-bar_element.ts',
options: [{transform: ['kebab']}],
errors: [
{
message: 'File name should be "foo-bar-element" but was "foo-bar_element"',
type: 'ClassDeclaration'
}
]
},
{
code,
filename: 'foo-bar-controller.ts',
options: [{transform: 'kebab', suffix: ['Element']}],
errors: [
{
message: 'File name should be "foo-bar-element" or "foo-bar" but was "foo-bar-controller"',
type: 'ClassDeclaration'
}
]
},
{
code,
filename: 'foo-bar-controller.ts',
options: [{transform: 'kebab', suffix: ['Controller']}],
errors: [
{
message: 'File name should be "foo-bar-element" but was "foo-bar-controller"',
type: 'ClassDeclaration'
}
]
}
]
})