diff --git a/docs/rules/filename-naming-convention.md b/docs/rules/filename-naming-convention.md index 9bb560e..8b674dd 100644 --- a/docs/rules/filename-naming-convention.md +++ b/docs/rules/filename-naming-convention.md @@ -72,6 +72,29 @@ module.exports = { }; ``` +#### using capture groups + +You can use glob capture groups in you rule set using the `` syntax. Read more about glob capture groups in the [micromatch documentation](https://github.com/micromatch/micromatch#capture). + +For example the following rule will only allow a file to be named the same as its parent folder : + +```js +module.exports = { + plugins: ['check-file'], + rules: { + 'check-file/filename-naming-convention': [ + 'error', + { + '**/*/*': '<1>', + }, + { + ignoreMiddleExtensions: true, + }, + ], + }, +}; +``` + #### rule configuration object ##### `ignoreMiddleExtensions` diff --git a/lib/rules/filename-naming-convention.js b/lib/rules/filename-naming-convention.js index b701191..44bffe0 100644 --- a/lib/rules/filename-naming-convention.js +++ b/lib/rules/filename-naming-convention.js @@ -4,10 +4,11 @@ */ 'use strict'; +const { transformRuleWithGroupCapture } = require('../utils/transform'); const { getFilename, getBasename, getFilePath } = require('../utils/filename'); const { checkSettings, - namingPatternValidator, + fileNamingPatternValidator, globPatternValidator, } = require('../utils/settings'); const { getDocUrl } = require('../utils/doc'); @@ -45,13 +46,16 @@ module.exports = { create(context) { return { Program: (node) => { - const rules = context.options[0]; + const filenameWithPath = getFilePath(context); + const filename = getFilename(filenameWithPath); + + const rules = context.options[0] || {}; const { ignoreMiddleExtensions } = context.options[1] || {}; const invalidPattern = checkSettings( rules, globPatternValidator, - namingPatternValidator + fileNamingPatternValidator ); if (invalidPattern) { @@ -66,29 +70,33 @@ module.exports = { return; } - const filenameWithPath = getFilePath(context); - const filename = getFilename(filenameWithPath); + for (const [ + originalFexPattern, + originalNamingPattern, + ] of Object.entries(rules)) { + try { + const [fexPattern, namingPattern] = transformRuleWithGroupCapture( + [originalFexPattern, originalNamingPattern], + filenameWithPath + ); - for (const [fexPattern, namingPattern] of Object.entries(rules)) { - const matchResult = matchRule( - filenameWithPath, - fexPattern, - getBasename(filename, ignoreMiddleExtensions), - namingPattern - ); + const matchResult = matchRule( + filenameWithPath, + fexPattern, + getBasename(filename, ignoreMiddleExtensions), + namingPattern + ); - if (matchResult) { - const { pattern } = matchResult; + if (matchResult) { + throw new Error( + `The filename "${filename}" does not match the "${originalNamingPattern}" style` + ); + } + } catch (error) { context.report({ node, - message: - 'The filename "{{filename}}" does not match the "{{pattern}}" style', - data: { - filename, - pattern, - }, + message: error.message, }); - return; } } }, diff --git a/lib/utils/settings.js b/lib/utils/settings.js index 34cab68..95e2c73 100644 --- a/lib/utils/settings.js +++ b/lib/utils/settings.js @@ -39,6 +39,16 @@ const namingPatternValidator = (namingPattern) => { return isGlob(namingPattern) || buildInPatterns.includes(namingPattern); }; +/** + * @returns {boolean} true if pattern is a valid naming pattern + * @param {string} namingPattern pattern string + */ +const fileNamingPatternValidator = (namingPattern) => { + return ( + namingPatternValidator(namingPattern) || !!/^<\d+>$/.test(namingPattern) + ); +}; + /** * @returns {boolean} true if pattern is a valid glob pattern * @param {string} pattern pattern string @@ -48,5 +58,6 @@ const globPatternValidator = isGlob; module.exports = { checkSettings, namingPatternValidator, + fileNamingPatternValidator, globPatternValidator, }; diff --git a/lib/utils/transform.js b/lib/utils/transform.js new file mode 100644 index 0000000..a7e27f7 --- /dev/null +++ b/lib/utils/transform.js @@ -0,0 +1,41 @@ +const micromatch = require('micromatch'); + +/** + * Takes in a ruleset and transforms it if it contains capture groups + * + * @param {Array} ruleset ruleset + * @param {Array} ruleset.0 glob + * @param {Array} ruleset.1 rule to transform + * @param {string} filenameWithPath filename with path + * @returns {Array} [glob, rule] + */ +function transformRuleWithGroupCapture([glob, rule], filenameWithPath) { + const keyCaptureGroups = micromatch.capture(glob, filenameWithPath); + + if (!keyCaptureGroups) { + return [glob, rule]; + } + + const valueCaptureGroupRegex = /<(\d+)>/g; + const valueCaptureGroups = [...rule.matchAll(valueCaptureGroupRegex)]; + + if (!valueCaptureGroups || !valueCaptureGroups.length) { + return [glob, rule]; + } + + const newRule = valueCaptureGroups.reduce((value, group) => { + const groupIndex = +group[1]; + if (!keyCaptureGroups || keyCaptureGroups[groupIndex] === undefined) { + throw new Error( + `The capture group "${rule}" is not found in the glob "${glob}"` + ); + } + return value.replace(group[0], keyCaptureGroups[+group[1]]); + }, rule); + + return [glob, newRule]; +} + +module.exports = { + transformRuleWithGroupCapture, +}; diff --git a/tests/lib/rules/filename-naming-convention.posix.js b/tests/lib/rules/filename-naming-convention.posix.js index f8323ca..1938c5c 100644 --- a/tests/lib/rules/filename-naming-convention.posix.js +++ b/tests/lib/rules/filename-naming-convention.posix.js @@ -1271,3 +1271,94 @@ ruleTester.run( ], } ); + +ruleTester.run( + "filename-naming-convention with option: [{ '**/*/!(index).*': '<1>' }, { ignoreMiddleExtensions: true }]", + rule, + { + valid: [ + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/index.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/featureA.jsx', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/featureA.specs.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + ], + + invalid: [ + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/featureB.jsx', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + errors: [ + { + message: + 'The filename "featureB.jsx" does not match the "<1>" style', + column: 1, + line: 1, + }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/featureB.specs.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + errors: [ + { + message: + 'The filename "featureB.specs.js" does not match the "<1>" style', + column: 1, + line: 1, + }, + ], + }, + ], + } +); + +ruleTester.run( + "filename-naming-convention with option: [{ '**/*/!(index).*': '<9>' }]", + rule, + { + valid: [], + invalid: [ + { + code: "var foo = 'bar';", + filename: 'src/components/featureA/featureA.jsx', + options: [{ '**/*/!(index).*': '<9>' }], + errors: [ + { + message: + 'The capture group "<9>" is not found in the glob "**/*/!(index).*"', + column: 1, + line: 1, + }, + ], + }, + ], + } +); diff --git a/tests/lib/rules/filename-naming-convention.windows.js b/tests/lib/rules/filename-naming-convention.windows.js index a64ba79..209fc93 100644 --- a/tests/lib/rules/filename-naming-convention.windows.js +++ b/tests/lib/rules/filename-naming-convention.windows.js @@ -828,3 +828,94 @@ ruleTester.run( ], } ); + +ruleTester.run( + "filename-naming-convention with option: [{ '**/*/!(index).*': '<1>' }, { ignoreMiddleExtensions: true }]", + rule, + { + valid: [ + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\index.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\featureA.jsx', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\featureA.specs.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + }, + ], + + invalid: [ + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\featureB.jsx', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + errors: [ + { + message: + 'The filename "featureB.jsx" does not match the "<1>" style', + column: 1, + line: 1, + }, + ], + }, + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\featureB.specs.js', + options: [ + { '**/*/!(index).*': '<1>' }, + { ignoreMiddleExtensions: true }, + ], + errors: [ + { + message: + 'The filename "featureB.specs.js" does not match the "<1>" style', + column: 1, + line: 1, + }, + ], + }, + ], + } +); + +ruleTester.run( + "filename-naming-convention with option: [{ '**/*/!(index).*': '<9>' }]", + rule, + { + valid: [], + invalid: [ + { + code: "var foo = 'bar';", + filename: 'src\\components\\featureA\\featureA.jsx', + options: [{ '**/*/!(index).*': '<9>' }], + errors: [ + { + message: + 'The capture group "<9>" is not found in the glob "**/*/!(index).*"', + column: 1, + line: 1, + }, + ], + }, + ], + } +);