From 4594020dc5e01ba1fca0c564d376c1f5c9e2f2ae Mon Sep 17 00:00:00 2001 From: michaelowolf <109518655+michaelowolf@users.noreply.github.com> Date: Thu, 22 Feb 2024 06:53:16 +0000 Subject: [PATCH] `filename-case`: Add option for multiple file extensions (#2186) Co-authored-by: fisker --- docs/rules/filename-case.md | 46 ++++++++++++ rules/filename-case.js | 49 +++++++++++-- test/filename-case.mjs | 102 +++++++++++++++++++++++--- test/snapshots/filename-case.mjs.md | 30 ++++---- test/snapshots/filename-case.mjs.snap | Bin 596 -> 592 bytes 5 files changed, 192 insertions(+), 35 deletions(-) diff --git a/docs/rules/filename-case.md b/docs/rules/filename-case.md index aedecd81b4..c4f19c3267 100644 --- a/docs/rules/filename-case.md +++ b/docs/rules/filename-case.md @@ -105,3 +105,49 @@ Don't forget that you must escape special characters that you don't want to be i } ] ``` + +### multipleFileExtensions + +Type: `boolean`\ +Default: `true` + +Whether to treat additional, `.`-separated parts of a filename as parts of the extension rather than parts of the filename. + +Note that the parts of the filename treated as the extension will not have the filename case enforced. + +For example: + +```js +"unicorn/filename-case": [ + "error", + { + "case": "pascalCase" + } +] + +// Results +✅ FooBar.Test.js +✅ FooBar.TestUtils.js +✅ FooBar.testUtils.js +✅ FooBar.test.js +✅ FooBar.test-utils.js +✅ FooBar.test_utils.js +``` + +```js +"unicorn/filename-case": [ + "error", + { + "case": "pascalCase", + "multipleFileExtensions": false + } +] + +// Results +✅ FooBar.Test.js +✅ FooBar.TestUtils.js +❌ FooBar.testUtils.js +❌ FooBar.test.js +❌ FooBar.test-utils.js +❌ FooBar.test_utils.js +``` diff --git a/rules/filename-case.js b/rules/filename-case.js index 88995f0d9e..9ec7cbed01 100644 --- a/rules/filename-case.js +++ b/rules/filename-case.js @@ -85,7 +85,7 @@ function validateFilename(words, caseFunctions) { .every(({word}) => caseFunctions.some(caseFunction => caseFunction(word) === word)); } -function fixFilename(words, caseFunctions, {leading, extension}) { +function fixFilename(words, caseFunctions, {leading, trailing}) { const replacements = words .map(({word, ignored}) => ignored ? [word] : caseFunctions.map(caseFunction => caseFunction(word))); @@ -93,7 +93,30 @@ function fixFilename(words, caseFunctions, {leading, extension}) { samples: combinations, } = cartesianProductSamples(replacements); - return [...new Set(combinations.map(parts => `${leading}${parts.join('')}${extension.toLowerCase()}`))]; + return [...new Set(combinations.map(parts => `${leading}${parts.join('')}${trailing}`))]; +} + +function getFilenameParts(filenameWithExtension, {multipleFileExtensions}) { + const extension = path.extname(filenameWithExtension); + const filename = path.basename(filenameWithExtension, extension); + const basename = filename + extension; + + const parts = { + basename, + filename, + middle: '', + extension, + }; + + if (multipleFileExtensions) { + const [firstPart] = filename.split('.'); + Object.assign(parts, { + filename: firstPart, + middle: filename.slice(firstPart.length), + }); + } + + return parts; } const leadingUnderscoresRegex = /^(?_+)(?.*)$/; @@ -143,6 +166,7 @@ const create = context => { return new RegExp(item, 'u'); }); + const multipleFileExtensions = options.multipleFileExtensions !== false; const chosenCasesFunctions = chosenCases.map(case_ => ignoreNumbers(cases[case_].fn)); const filenameWithExtension = context.physicalFilename; @@ -152,11 +176,14 @@ const create = context => { return { Program() { - const extension = path.extname(filenameWithExtension); - const filename = path.basename(filenameWithExtension, extension); - const base = filename + extension; + const { + basename, + filename, + middle, + extension, + } = getFilenameParts(filenameWithExtension, {multipleFileExtensions}); - if (ignoredByDefault.has(base) || ignore.some(regexp => regexp.test(base))) { + if (ignoredByDefault.has(basename) || ignore.some(regexp => regexp.test(basename))) { return; } @@ -168,7 +195,7 @@ const create = context => { return { loc: {column: 0, line: 1}, messageId: MESSAGE_ID_EXTENSION, - data: {filename: filename + extension.toLowerCase(), extension}, + data: {filename: filename + middle + extension.toLowerCase(), extension}, }; } @@ -177,7 +204,7 @@ const create = context => { const renamedFilenames = fixFilename(words, chosenCasesFunctions, { leading, - extension, + trailing: middle + extension.toLowerCase(), }); return { @@ -211,6 +238,9 @@ const schema = [ type: 'array', uniqueItems: true, }, + multipleFileExtensions: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -237,6 +267,9 @@ const schema = [ type: 'array', uniqueItems: true, }, + multipleFileExtensions: { + type: 'boolean', + }, }, additionalProperties: false, }, diff --git a/test/filename-case.mjs b/test/filename-case.mjs index 81ab56ee9c..26c3969b83 100644 --- a/test/filename-case.mjs +++ b/test/filename-case.mjs @@ -20,7 +20,7 @@ function testManyCases(filename, chosenCases, errorMessage) { function testCaseWithOptions(filename, errorMessage, options = []) { return { - code: 'foo()', + code: `/* Filename ${filename} */`, filename, options, errors: errorMessage && [ @@ -37,22 +37,30 @@ test({ 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/fooBar.test-utils.js', 'camelCase'), + testCase('src/foo/fooBar.test_utils.js', 'camelCase'), + testCase('src/foo/.test_utils.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_bar.test-utils.js', 'snakeCase'), + testCase('src/foo/.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-bar.test_utils.js', 'kebabCase'), + testCase('src/foo/.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('src/foo/Foo.test.js', 'pascalCase'), + testCase('src/foo/FooBar.test.js', 'pascalCase'), + testCase('src/foo/FooBar.test-utils.js', 'pascalCase'), + testCase('src/foo/FooBar.test_utils.js', 'pascalCase'), + testCase('src/foo/.test_utils.js', 'pascalCase'), testCase('spec/iss47Spec.js', 'camelCase'), testCase('spec/iss47Spec100.js', 'camelCase'), testCase('spec/i18n.js', 'camelCase'), @@ -65,7 +73,7 @@ test({ testCase('spec/iss47_100spec.js', 'snakeCase'), testCase('spec/i18n.js', 'snakeCase'), testCase('spec/Iss47Spec.js', 'pascalCase'), - testCase('spec/Iss47.100Spec.js', 'pascalCase'), + testCase('spec/Iss47.100spec.js', 'pascalCase'), testCase('spec/I18n.js', 'pascalCase'), testCase(undefined, 'camelCase'), testCase(undefined, 'snakeCase'), @@ -238,6 +246,31 @@ test({ ...['index.js', 'index.mjs', 'index.cjs', 'index.ts', 'index.tsx', 'index.vue'].flatMap( filename => ['camelCase', 'snakeCase', 'kebabCase', 'pascalCase'].map(chosenCase => testCase(filename, chosenCase)), ), + testCaseWithOptions('index.tsx', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/index.tsx', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/fooBar.test.js', undefined, [{case: 'camelCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/fooBar.testUtils.js', undefined, [{case: 'camelCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/foo_bar.test_utils.js', undefined, [{case: 'snakeCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/foo.test.js', undefined, [{case: 'kebabCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/foo-bar.test.js', undefined, [{case: 'kebabCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/foo-bar.test-utils.js', undefined, [{case: 'kebabCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/Foo.Test.js', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/FooBar.Test.js', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + testCaseWithOptions('src/foo/FooBar.TestUtils.js', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + testCaseWithOptions('spec/Iss47.100Spec.js', undefined, [{case: 'pascalCase', multipleFileExtensions: false}]), + // Multiple filename parts - multiple file extensions + testCaseWithOptions('src/foo/fooBar.Test.js', undefined, [{case: 'camelCase'}]), + testCaseWithOptions('test/foo/fooBar.testUtils.js', undefined, [{case: 'camelCase'}]), + testCaseWithOptions('test/foo/.testUtils.js', undefined, [{case: 'camelCase'}]), + testCaseWithOptions('test/foo/foo_bar.Test.js', undefined, [{case: 'snakeCase'}]), + testCaseWithOptions('test/foo/foo_bar.Test_Utils.js', undefined, [{case: 'snakeCase'}]), + testCaseWithOptions('test/foo/.Test_Utils.js', undefined, [{case: 'snakeCase'}]), + testCaseWithOptions('test/foo/foo-bar.Test.js', undefined, [{case: 'kebabCase'}]), + testCaseWithOptions('test/foo/foo-bar.Test-Utils.js', undefined, [{case: 'kebabCase'}]), + testCaseWithOptions('test/foo/.Test-Utils.js', undefined, [{case: 'kebabCase'}]), + testCaseWithOptions('test/foo/FooBar.Test.js', undefined, [{case: 'pascalCase'}]), + testCaseWithOptions('test/foo/FooBar.TestUtils.js', undefined, [{case: 'pascalCase'}]), + testCaseWithOptions('test/foo/.TestUtils.js', undefined, [{case: 'pascalCase'}]), ], invalid: [ testCase( @@ -258,7 +291,7 @@ test({ testCase( 'test/foo/foo_bar.test_utils.js', 'camelCase', - 'Filename is not in camel case. Rename it to `fooBar.testUtils.js`.', + 'Filename is not in camel case. Rename it to `fooBar.test_utils.js`.', ), testCase( 'test/foo/fooBar.js', @@ -273,7 +306,7 @@ test({ testCase( 'test/foo/fooBar.testUtils.js', 'snakeCase', - 'Filename is not in snake case. Rename it to `foo_bar.test_utils.js`.', + 'Filename is not in snake case. Rename it to `foo_bar.testUtils.js`.', ), testCase( 'test/foo/fooBar.js', @@ -288,7 +321,7 @@ test({ testCase( 'test/foo/fooBar.testUtils.js', 'kebabCase', - 'Filename is not in kebab case. Rename it to `foo-bar.test-utils.js`.', + 'Filename is not in kebab case. Rename it to `foo-bar.testUtils.js`.', ), testCase( 'test/foo/fooBar.js', @@ -298,12 +331,12 @@ test({ testCase( 'test/foo/foo_bar.test.js', 'pascalCase', - 'Filename is not in pascal case. Rename it to `FooBar.Test.js`.', + '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`.', + 'Filename is not in pascal case. Rename it to `FooBar.test-utils.js`.', ), testCase( 'src/foo/_FOO-BAR.js', @@ -547,6 +580,47 @@ test({ }, 'Filename is not in camel case, pascal case, or kebab case. Rename it to `1.js`.', ), + // Multiple filename parts - single file extension + testCaseWithOptions( + 'src/foo/foo_bar.test.js', + 'Filename is not in camel case. Rename it to `fooBar.test.js`.', + [{case: 'camelCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/foo_bar.test_utils.js', + 'Filename is not in camel case. Rename it to `fooBar.testUtils.js`.', + [{case: 'camelCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/fooBar.test.js', + 'Filename is not in snake case. Rename it to `foo_bar.test.js`.', + [{case: 'snakeCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/fooBar.testUtils.js', + 'Filename is not in snake case. Rename it to `foo_bar.test_utils.js`.', + [{case: 'snakeCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/fooBar.test.js', + 'Filename is not in kebab case. Rename it to `foo-bar.test.js`.', + [{case: 'kebabCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/fooBar.testUtils.js', + 'Filename is not in kebab case. Rename it to `foo-bar.test-utils.js`.', + [{case: 'kebabCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/foo_bar.test.js', + 'Filename is not in pascal case. Rename it to `FooBar.Test.js`.', + [{case: 'pascalCase', multipleFileExtensions: false}], + ), + testCaseWithOptions( + 'test/foo/foo-bar.test-utils.js', + 'Filename is not in pascal case. Rename it to `FooBar.TestUtils.js`.', + [{case: 'pascalCase', multipleFileExtensions: false}], + ), ], }); @@ -554,7 +628,11 @@ test.snapshot({ valid: [ undefined, 'src/foo.JS/bar.js', + 'src/foo.JS/bar.spec.js', + 'src/foo.JS/.spec.js', 'src/foo.JS/bar', + 'foo.SPEC.js', + '.SPEC.js', ].map(filename => ({code: `const filename = ${JSON.stringify(filename)};`, filename})), invalid: [ { @@ -575,6 +653,6 @@ test.snapshot({ 'foo.jS', 'index.JS', 'foo..JS', - ].map(filename => ({code: `const filename = ${JSON.stringify(filename)};`, filename})), + ].map(filename => ({code: `/* Filename ${filename} */`, filename})), ], }); diff --git a/test/snapshots/filename-case.mjs.md b/test/snapshots/filename-case.mjs.md index 2a818b53c6..e5bc3362e2 100644 --- a/test/snapshots/filename-case.mjs.md +++ b/test/snapshots/filename-case.mjs.md @@ -58,12 +58,12 @@ Generated by [AVA](https://avajs.dev). 11 |␊ ` -## invalid(2): const filename = "foo.JS"; +## invalid(2): /* Filename foo.JS */ > Input `␊ - 1 | const filename = "foo.JS";␊ + 1 | /* Filename foo.JS */␊ ` > Filename @@ -75,16 +75,16 @@ Generated by [AVA](https://avajs.dev). > Error 1/1 `␊ - > 1 | const filename = "foo.JS";␊ + > 1 | /* Filename foo.JS */␊ | ^ File extension \`.JS\` is not in lowercase. Rename it to \`foo.js\`.␊ ` -## invalid(3): const filename = "foo.Js"; +## invalid(3): /* Filename foo.Js */ > Input `␊ - 1 | const filename = "foo.Js";␊ + 1 | /* Filename foo.Js */␊ ` > Filename @@ -96,16 +96,16 @@ Generated by [AVA](https://avajs.dev). > Error 1/1 `␊ - > 1 | const filename = "foo.Js";␊ + > 1 | /* Filename foo.Js */␊ | ^ File extension \`.Js\` is not in lowercase. Rename it to \`foo.js\`.␊ ` -## invalid(4): const filename = "foo.jS"; +## invalid(4): /* Filename foo.jS */ > Input `␊ - 1 | const filename = "foo.jS";␊ + 1 | /* Filename foo.jS */␊ ` > Filename @@ -117,16 +117,16 @@ Generated by [AVA](https://avajs.dev). > Error 1/1 `␊ - > 1 | const filename = "foo.jS";␊ + > 1 | /* Filename foo.jS */␊ | ^ File extension \`.jS\` is not in lowercase. Rename it to \`foo.js\`.␊ ` -## invalid(5): const filename = "index.JS"; +## invalid(5): /* Filename index.JS */ > Input `␊ - 1 | const filename = "index.JS";␊ + 1 | /* Filename index.JS */␊ ` > Filename @@ -138,16 +138,16 @@ Generated by [AVA](https://avajs.dev). > Error 1/1 `␊ - > 1 | const filename = "index.JS";␊ + > 1 | /* Filename index.JS */␊ | ^ File extension \`.JS\` is not in lowercase. Rename it to \`index.js\`.␊ ` -## invalid(6): const filename = "foo..JS"; +## invalid(6): /* Filename foo..JS */ > Input `␊ - 1 | const filename = "foo..JS";␊ + 1 | /* Filename foo..JS */␊ ` > Filename @@ -159,6 +159,6 @@ Generated by [AVA](https://avajs.dev). > Error 1/1 `␊ - > 1 | const filename = "foo..JS";␊ + > 1 | /* Filename foo..JS */␊ | ^ File extension \`.JS\` is not in lowercase. Rename it to \`foo..js\`.␊ ` diff --git a/test/snapshots/filename-case.mjs.snap b/test/snapshots/filename-case.mjs.snap index 41246e5b28b302177fa5929a3932a61b28878281..f2c766bdb101eed86568bd7ba74c5bbb5cb119c7 100644 GIT binary patch literal 592 zcmV-W0ZtNH0xk9o#S4AoTzOiChpw;zJ0? zI$4u-o!!X0Es+Q(5Wj#wK>3fH(K>6@Q|$&-&H+o7^{!_;v(LQVUrC(F!IAoL$~4y* zo4(-Ugl2rW7PUH%NV(Q}So6V^pc6HcT75qq4ymSZeqRo*1mf_F#Y)v-r7plq)nla=vrK583i7q>kg^Fn)# e(ytL4>_K*dqDLN=E%Eqr1N#g9^Q|c02mk;={vf>o literal 596 zcmV-a0;~N&RzV3fHk)5^hpsZUdK48hR-u3Lv>@%;EpHZ0LaIC+d2_sD+ zW-p}r#1lDc1??_Gh^_X66(3S7KGjEP^tZFoh#UUq*X72QMs_vV8rLrUXfOZ>U~c0x z)gfw!s9mD=h}tLWE>ZW0x?fhoHcY3UzMsD3{Qdy=NfR<|i@D}X=