Skip to content

[New] Add filename-case rule #305

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

Closed
wants to merge 1 commit into from
Closed
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 @@ -181,6 +181,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi

| | Rule ID | Description |
|:---|:--------|:------------|
| | [filename-case](./docs/rules/filename-case.md) | enforce file-name conventions |
| :wrench: | [html-quotes](./docs/rules/html-quotes.md) | enforce quotes style of HTML attributes |
| | [no-confusing-v-for-v-if](./docs/rules/no-confusing-v-for-v-if.md) | disallow confusing `v-for` and `v-if` on the same element |
| | [order-in-components](./docs/rules/order-in-components.md) | enforce order of properties in components |
Expand Down
51 changes: 51 additions & 0 deletions docs/rules/filename-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# enforce file-name conventions (filename-case)

eslint-plugin-unicorn has a rule to check filename formats. However Vue apps have a convention of .vue files being `pascalCase` and and .js files not being `pascalCase`. This rule can be used as an alternative allowing the file format for files to be configured, but all .vue files will be set to pascalCase.

## :book: Rule Details

This ruleall linted files to have their names in a certain case style. Default is `kebabCase`.

Files named `index.js` are ignored as they can't change case (Only a problem with `pascalCase`).

### `kebabCase`

- `foo-bar.js`
- `foo-bar.test.js`
- `foo-bar.test-utils.js`

### `camelCase`

- `fooBar.js`
- `fooBar.test.js`
- `fooBar.testUtils.js`

### `snakeCase`

- `foo_bar.js`
- `foo_bar.test.js`
- `foo_bar.test_utils.js`

### `pascalCase`

- `FooBar.js`
- `FooBar.Test.js`
- `FooBar.TestUtils.js`


## Options

## :wrench: Options

```json
{
"vue/filename-case": [
"error",
{
"case": "kebabCase"
}
]
}
```

- `case` (`string"`) ... The filename case. Default is `kebabCase`.
1 change: 1 addition & 0 deletions lib/recommended-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
module.exports = {
"vue/attribute-hyphenation": "error",
"vue/filename-case": "error",
"vue/html-end-tags": "error",
"vue/html-indent": "error",
"vue/html-quotes": "error",
Expand Down
132 changes: 132 additions & 0 deletions lib/rules/filename-case.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict'
const path = require('path')
const camelCase = require('lodash.camelcase')
const kebabCase = require('lodash.kebabcase')
const snakeCase = require('lodash.snakecase')
const upperfirst = require('lodash.upperfirst')

const pascalCase = str => upperfirst(camelCase(str))
const numberRegex = /(\d+)/
const PLACEHOLDER = '\uFFFF\uFFFF\uFFFF'
const PLACEHOLDER_REGEX = new RegExp(PLACEHOLDER, 'i')

function ignoreNumbers (fn) {
return str => {
const stack = []
let execResult = numberRegex.exec(str)

while (execResult) {
stack.push(execResult[0])
str = str.replace(execResult[0], PLACEHOLDER)
execResult = numberRegex.exec(str)
}

let withCase = fn(str)

while (stack.length > 0) {
withCase = withCase.replace(PLACEHOLDER_REGEX, stack.shift())
}

return withCase
}
}

const cases = {
camelCase: {
fn: camelCase,
name: 'camel case'
},
kebabCase: {
fn: kebabCase,
name: 'kebab case'
},
snakeCase: {
fn: snakeCase,
name: 'snake case'
},
pascalCase: {
fn: pascalCase,
name: 'pascal case'
}
}

function fixFilename (chosenCase, filename) {
return filename
.split('.')
.map(ignoreNumbers(chosenCase.fn))
.join('.')
}

const leadingUnserscoresRegex = /^(_+)(.*)$/
function splitFilename (filename) {
const res = leadingUnserscoresRegex.exec(filename)
return {
leading: (res && res[1]) || '',
trailing: (res && res[2]) || filename
}
}

const create = context => {
let chosenCase = cases['camelCase']

if (context.options[0] && context.options[0].case) {
chosenCase = cases[context.options[0].case]
}

const filenameWithExt = context.getFilename()

if (filenameWithExt === '<text>') {
return {}
}

return {
Program: node => {
const extension = path.extname(filenameWithExt)
const filename = path.basename(filenameWithExt, extension)

if (filename + extension === 'index.js') {
return
}

if (extension === '.vue') {
chosenCase = cases['pascalCase']
}

const splitName = splitFilename(filename)
const fixedFilename = fixFilename(chosenCase, splitName.trailing)
const renameFilename = splitName.leading + fixedFilename + extension

if (fixedFilename !== splitName.trailing) {
context.report({
node,
message: `Filename is not in ${chosenCase.name}. Rename it to \`${renameFilename}\`.`
})
}
}
}
}

const schema = [{
type: 'object',
properties: {
case: {
enum: [
'camelCase',
'snakeCase',
'kebabCase',
'pascalCase'
]
}
}
}]

module.exports = {
create,
meta: {
docs: {
description: 'enforce file-name conventions',
category: 'recommended'
},
schema
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
},
"dependencies": {
"require-all": "^2.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"vue-eslint-parser": "^2.0.1-beta.1"
},
"devDependencies": {
Expand Down
173 changes: 173 additions & 0 deletions tests/lib/rules/filename-case.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @fileoverview Enforces component's data property to be a function.
* @author Armano
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const rule = require('../../../lib/rules/filename-case')

const RuleTester = require('eslint').RuleTester
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({
parser: 'vue-eslint-parser',
parserOptions: { ecmaVersion: 2015 }
})

function testCase (filename, chosenCase, errorMessage) {
return {
code: 'foo()',
filename,
options: [{ case: chosenCase }],
errors: errorMessage && [{
ruleId: 'filename-case',
message: errorMessage
}]
}
}

ruleTester.run('filename-case', rule, {
valid: [
testCase('src/foo/bar.js', 'camelCase'),
testCase('src/foo/fooBar.js', 'camelCase'),
testCase('src/foo/bar.test.js', 'camelCase'),
testCase('src/foo/fooBar.test.js', 'camelCase'),
testCase('src/foo/fooBar.testUtils.js', 'camelCase'),
testCase('src/foo/foo.js', 'snakeCase'),
testCase('src/foo/foo_bar.js', 'snakeCase'),
testCase('src/foo/foo.test.js', 'snakeCase'),
testCase('src/foo/foo_bar.test.js', 'snakeCase'),
testCase('src/foo/foo_bar.test_utils.js', 'snakeCase'),
testCase('src/foo/foo.js', 'kebabCase'),
testCase('src/foo/foo-bar.js', 'kebabCase'),
testCase('src/foo/foo.test.js', 'kebabCase'),
testCase('src/foo/foo-bar.test.js', 'kebabCase'),
testCase('src/foo/foo-bar.test-utils.js', 'kebabCase'),
testCase('src/foo/Foo.js', 'pascalCase'),
testCase('src/foo/FooBar.js', 'pascalCase'),
testCase('src/foo/Foo.Test.js', 'pascalCase'),
testCase('src/foo/FooBar.Test.js', 'pascalCase'),
testCase('src/foo/FooBar.TestUtils.js', 'pascalCase'),
testCase('spec/iss47Spec.js', 'camelCase'),
testCase('spec/iss47Spec100.js', 'camelCase'),
testCase('spec/i18n.js', 'camelCase'),
testCase('spec/iss47-spec.js', 'kebabCase'),
testCase('spec/iss-47-spec.js', 'kebabCase'),
testCase('spec/iss47-100spec.js', 'kebabCase'),
testCase('spec/i18n.js', 'kebabCase'),
testCase('spec/iss47_spec.js', 'snakeCase'),
testCase('spec/iss_47_spec.js', 'snakeCase'),
testCase('spec/iss47_100spec.js', 'snakeCase'),
testCase('spec/i18n.js', 'snakeCase'),
testCase('spec/Iss47Spec.js', 'pascalCase'),
testCase('spec/Iss47.100Spec.js', 'pascalCase'),
testCase('spec/I18n.js', 'pascalCase'),
testCase('spec/index.js', 'pascalCase'),
testCase('<text>', 'camelCase'),
testCase('<text>', 'snakeCase'),
testCase('<text>', 'kebabCase'),
testCase('<text>', 'pascalCase'),
testCase('src/foo/_fooBar.js', 'camelCase'),
testCase('src/foo/___fooBar.js', 'camelCase'),
testCase('src/foo/_foo_bar.js', 'snakeCase'),
testCase('src/foo/___foo_bar.js', 'snakeCase'),
testCase('src/foo/_foo-bar.js', 'kebabCase'),
testCase('src/foo/___foo-bar.js', 'kebabCase'),
testCase('src/foo/_FooBar.js', 'pascalCase'),
testCase('src/foo/___FooBar.js', 'pascalCase'),
testCase('App.vue', 'kebabCase'),
testCase('HelloWorld.vue', 'kebabCase')
],
invalid: [
testCase('src/foo/foo_bar.js',
undefined,
'Filename is not in camel case. Rename it to `fooBar.js`.'
),
testCase('src/foo/foo_bar.js',
'camelCase',
'Filename is not in camel case. Rename it to `fooBar.js`.'
),
testCase('src/foo/foo_bar.test.js',
'camelCase',
'Filename is not in camel case. Rename it to `fooBar.test.js`.'
),
testCase('test/foo/foo_bar.test_utils.js',
'camelCase',
'Filename is not in camel case. Rename it to `fooBar.testUtils.js`.'
),
testCase('test/foo/fooBar.js',
'snakeCase',
'Filename is not in snake case. Rename it to `foo_bar.js`.'
),
testCase('test/foo/fooBar.test.js',
'snakeCase',
'Filename is not in snake case. Rename it to `foo_bar.test.js`.'
),
testCase('test/foo/fooBar.testUtils.js',
'snakeCase',
'Filename is not in snake case. Rename it to `foo_bar.test_utils.js`.'
),
testCase('test/foo/fooBar.js',
'kebabCase',
'Filename is not in kebab case. Rename it to `foo-bar.js`.'
),
testCase('test/foo/fooBar.test.js',
'kebabCase',
'Filename is not in kebab case. Rename it to `foo-bar.test.js`.'
),
testCase('test/foo/fooBar.testUtils.js',
'kebabCase',
'Filename is not in kebab case. Rename it to `foo-bar.test-utils.js`.'
),
testCase('test/foo/fooBar.js',
'pascalCase',
'Filename is not in pascal case. Rename it to `FooBar.js`.'
),
testCase('test/foo/foo_bar.test.js',
'pascalCase',
'Filename is not in pascal case. Rename it to `FooBar.Test.js`.'
),
testCase('test/foo/foo-bar.test-utils.js',
'pascalCase',
'Filename is not in pascal case. Rename it to `FooBar.TestUtils.js`.'
),
testCase('src/foo/_FOO-BAR.js',
'camelCase',
'Filename is not in camel case. Rename it to `_fooBar.js`.'
),
testCase('src/foo/___FOO-BAR.js',
'camelCase',
'Filename is not in camel case. Rename it to `___fooBar.js`.'
),
testCase('src/foo/_FOO-BAR.js',
'snakeCase',
'Filename is not in snake case. Rename it to `_foo_bar.js`.'
),
testCase('src/foo/___FOO-BAR.js',
'snakeCase',
'Filename is not in snake case. Rename it to `___foo_bar.js`.'
),
testCase('src/foo/_FOO-BAR.js',
'kebabCase',
'Filename is not in kebab case. Rename it to `_foo-bar.js`.'
),
testCase('src/foo/___FOO-BAR.js',
'kebabCase',
'Filename is not in kebab case. Rename it to `___foo-bar.js`.'
),
testCase('src/foo/_FOO-BAR.js',
'pascalCase',
'Filename is not in pascal case. Rename it to `_FooBar.js`.'
),
testCase('src/foo/___FOO-BAR.js',
'pascalCase',
'Filename is not in pascal case. Rename it to `___FooBar.js`.'
)
]
})