Skip to content

Commit 572c535

Browse files
authored
feat(#17): add group capture support for rule filename-naming-convention
* feat: implement group capture in filename-naming-convention * test: add tests for group capture * docs: update docs for group capture
1 parent 066ccf7 commit 572c535

File tree

6 files changed

+286
-21
lines changed

6 files changed

+286
-21
lines changed

docs/rules/filename-naming-convention.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,29 @@ module.exports = {
7272
};
7373
```
7474

75+
#### using capture groups
76+
77+
You can use glob capture groups in you rule set using the `<index>` syntax. Read more about glob capture groups in the [micromatch documentation](https://github.com/micromatch/micromatch#capture).
78+
79+
For example the following rule will only allow a file to be named the same as its parent folder :
80+
81+
```js
82+
module.exports = {
83+
plugins: ['check-file'],
84+
rules: {
85+
'check-file/filename-naming-convention': [
86+
'error',
87+
{
88+
'**/*/*': '<1>',
89+
},
90+
{
91+
ignoreMiddleExtensions: true,
92+
},
93+
],
94+
},
95+
};
96+
```
97+
7598
#### rule configuration object
7699

77100
##### `ignoreMiddleExtensions`

lib/rules/filename-naming-convention.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
*/
55
'use strict';
66

7+
const { transformRuleWithGroupCapture } = require('../utils/transform');
78
const { getFilename, getBasename, getFilePath } = require('../utils/filename');
89
const {
910
checkSettings,
10-
namingPatternValidator,
11+
fileNamingPatternValidator,
1112
globPatternValidator,
1213
} = require('../utils/settings');
1314
const { getDocUrl } = require('../utils/doc');
@@ -45,13 +46,16 @@ module.exports = {
4546
create(context) {
4647
return {
4748
Program: (node) => {
48-
const rules = context.options[0];
49+
const filenameWithPath = getFilePath(context);
50+
const filename = getFilename(filenameWithPath);
51+
52+
const rules = context.options[0] || {};
4953
const { ignoreMiddleExtensions } = context.options[1] || {};
5054

5155
const invalidPattern = checkSettings(
5256
rules,
5357
globPatternValidator,
54-
namingPatternValidator
58+
fileNamingPatternValidator
5559
);
5660

5761
if (invalidPattern) {
@@ -66,29 +70,33 @@ module.exports = {
6670
return;
6771
}
6872

69-
const filenameWithPath = getFilePath(context);
70-
const filename = getFilename(filenameWithPath);
73+
for (const [
74+
originalFexPattern,
75+
originalNamingPattern,
76+
] of Object.entries(rules)) {
77+
try {
78+
const [fexPattern, namingPattern] = transformRuleWithGroupCapture(
79+
[originalFexPattern, originalNamingPattern],
80+
filenameWithPath
81+
);
7182

72-
for (const [fexPattern, namingPattern] of Object.entries(rules)) {
73-
const matchResult = matchRule(
74-
filenameWithPath,
75-
fexPattern,
76-
getBasename(filename, ignoreMiddleExtensions),
77-
namingPattern
78-
);
83+
const matchResult = matchRule(
84+
filenameWithPath,
85+
fexPattern,
86+
getBasename(filename, ignoreMiddleExtensions),
87+
namingPattern
88+
);
7989

80-
if (matchResult) {
81-
const { pattern } = matchResult;
90+
if (matchResult) {
91+
throw new Error(
92+
`The filename "${filename}" does not match the "${originalNamingPattern}" style`
93+
);
94+
}
95+
} catch (error) {
8296
context.report({
8397
node,
84-
message:
85-
'The filename "{{filename}}" does not match the "{{pattern}}" style',
86-
data: {
87-
filename,
88-
pattern,
89-
},
98+
message: error.message,
9099
});
91-
return;
92100
}
93101
}
94102
},

lib/utils/settings.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ const namingPatternValidator = (namingPattern) => {
3939
return isGlob(namingPattern) || buildInPatterns.includes(namingPattern);
4040
};
4141

42+
/**
43+
* @returns {boolean} true if pattern is a valid naming pattern
44+
* @param {string} namingPattern pattern string
45+
*/
46+
const fileNamingPatternValidator = (namingPattern) => {
47+
return (
48+
namingPatternValidator(namingPattern) || !!/^<\d+>$/.test(namingPattern)
49+
);
50+
};
51+
4252
/**
4353
* @returns {boolean} true if pattern is a valid glob pattern
4454
* @param {string} pattern pattern string
@@ -48,5 +58,6 @@ const globPatternValidator = isGlob;
4858
module.exports = {
4959
checkSettings,
5060
namingPatternValidator,
61+
fileNamingPatternValidator,
5162
globPatternValidator,
5263
};

lib/utils/transform.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const micromatch = require('micromatch');
2+
3+
/**
4+
* Takes in a ruleset and transforms it if it contains capture groups
5+
*
6+
* @param {Array} ruleset ruleset
7+
* @param {Array} ruleset.0 glob
8+
* @param {Array} ruleset.1 rule to transform
9+
* @param {string} filenameWithPath filename with path
10+
* @returns {Array} [glob, rule]
11+
*/
12+
function transformRuleWithGroupCapture([glob, rule], filenameWithPath) {
13+
const keyCaptureGroups = micromatch.capture(glob, filenameWithPath);
14+
15+
if (!keyCaptureGroups) {
16+
return [glob, rule];
17+
}
18+
19+
const valueCaptureGroupRegex = /<(\d+)>/g;
20+
const valueCaptureGroups = [...rule.matchAll(valueCaptureGroupRegex)];
21+
22+
if (!valueCaptureGroups || !valueCaptureGroups.length) {
23+
return [glob, rule];
24+
}
25+
26+
const newRule = valueCaptureGroups.reduce((value, group) => {
27+
const groupIndex = +group[1];
28+
if (!keyCaptureGroups || keyCaptureGroups[groupIndex] === undefined) {
29+
throw new Error(
30+
`The capture group "${rule}" is not found in the glob "${glob}"`
31+
);
32+
}
33+
return value.replace(group[0], keyCaptureGroups[+group[1]]);
34+
}, rule);
35+
36+
return [glob, newRule];
37+
}
38+
39+
module.exports = {
40+
transformRuleWithGroupCapture,
41+
};

tests/lib/rules/filename-naming-convention.posix.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,3 +1271,94 @@ ruleTester.run(
12711271
],
12721272
}
12731273
);
1274+
1275+
ruleTester.run(
1276+
"filename-naming-convention with option: [{ '**/*/!(index).*': '<1>' }, { ignoreMiddleExtensions: true }]",
1277+
rule,
1278+
{
1279+
valid: [
1280+
{
1281+
code: "var foo = 'bar';",
1282+
filename: 'src/components/featureA/index.js',
1283+
options: [
1284+
{ '**/*/!(index).*': '<1>' },
1285+
{ ignoreMiddleExtensions: true },
1286+
],
1287+
},
1288+
{
1289+
code: "var foo = 'bar';",
1290+
filename: 'src/components/featureA/featureA.jsx',
1291+
options: [
1292+
{ '**/*/!(index).*': '<1>' },
1293+
{ ignoreMiddleExtensions: true },
1294+
],
1295+
},
1296+
{
1297+
code: "var foo = 'bar';",
1298+
filename: 'src/components/featureA/featureA.specs.js',
1299+
options: [
1300+
{ '**/*/!(index).*': '<1>' },
1301+
{ ignoreMiddleExtensions: true },
1302+
],
1303+
},
1304+
],
1305+
1306+
invalid: [
1307+
{
1308+
code: "var foo = 'bar';",
1309+
filename: 'src/components/featureA/featureB.jsx',
1310+
options: [
1311+
{ '**/*/!(index).*': '<1>' },
1312+
{ ignoreMiddleExtensions: true },
1313+
],
1314+
errors: [
1315+
{
1316+
message:
1317+
'The filename "featureB.jsx" does not match the "<1>" style',
1318+
column: 1,
1319+
line: 1,
1320+
},
1321+
],
1322+
},
1323+
{
1324+
code: "var foo = 'bar';",
1325+
filename: 'src/components/featureA/featureB.specs.js',
1326+
options: [
1327+
{ '**/*/!(index).*': '<1>' },
1328+
{ ignoreMiddleExtensions: true },
1329+
],
1330+
errors: [
1331+
{
1332+
message:
1333+
'The filename "featureB.specs.js" does not match the "<1>" style',
1334+
column: 1,
1335+
line: 1,
1336+
},
1337+
],
1338+
},
1339+
],
1340+
}
1341+
);
1342+
1343+
ruleTester.run(
1344+
"filename-naming-convention with option: [{ '**/*/!(index).*': '<9>' }]",
1345+
rule,
1346+
{
1347+
valid: [],
1348+
invalid: [
1349+
{
1350+
code: "var foo = 'bar';",
1351+
filename: 'src/components/featureA/featureA.jsx',
1352+
options: [{ '**/*/!(index).*': '<9>' }],
1353+
errors: [
1354+
{
1355+
message:
1356+
'The capture group "<9>" is not found in the glob "**/*/!(index).*"',
1357+
column: 1,
1358+
line: 1,
1359+
},
1360+
],
1361+
},
1362+
],
1363+
}
1364+
);

tests/lib/rules/filename-naming-convention.windows.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,3 +828,94 @@ ruleTester.run(
828828
],
829829
}
830830
);
831+
832+
ruleTester.run(
833+
"filename-naming-convention with option: [{ '**/*/!(index).*': '<1>' }, { ignoreMiddleExtensions: true }]",
834+
rule,
835+
{
836+
valid: [
837+
{
838+
code: "var foo = 'bar';",
839+
filename: 'src\\components\\featureA\\index.js',
840+
options: [
841+
{ '**/*/!(index).*': '<1>' },
842+
{ ignoreMiddleExtensions: true },
843+
],
844+
},
845+
{
846+
code: "var foo = 'bar';",
847+
filename: 'src\\components\\featureA\\featureA.jsx',
848+
options: [
849+
{ '**/*/!(index).*': '<1>' },
850+
{ ignoreMiddleExtensions: true },
851+
],
852+
},
853+
{
854+
code: "var foo = 'bar';",
855+
filename: 'src\\components\\featureA\\featureA.specs.js',
856+
options: [
857+
{ '**/*/!(index).*': '<1>' },
858+
{ ignoreMiddleExtensions: true },
859+
],
860+
},
861+
],
862+
863+
invalid: [
864+
{
865+
code: "var foo = 'bar';",
866+
filename: 'src\\components\\featureA\\featureB.jsx',
867+
options: [
868+
{ '**/*/!(index).*': '<1>' },
869+
{ ignoreMiddleExtensions: true },
870+
],
871+
errors: [
872+
{
873+
message:
874+
'The filename "featureB.jsx" does not match the "<1>" style',
875+
column: 1,
876+
line: 1,
877+
},
878+
],
879+
},
880+
{
881+
code: "var foo = 'bar';",
882+
filename: 'src\\components\\featureA\\featureB.specs.js',
883+
options: [
884+
{ '**/*/!(index).*': '<1>' },
885+
{ ignoreMiddleExtensions: true },
886+
],
887+
errors: [
888+
{
889+
message:
890+
'The filename "featureB.specs.js" does not match the "<1>" style',
891+
column: 1,
892+
line: 1,
893+
},
894+
],
895+
},
896+
],
897+
}
898+
);
899+
900+
ruleTester.run(
901+
"filename-naming-convention with option: [{ '**/*/!(index).*': '<9>' }]",
902+
rule,
903+
{
904+
valid: [],
905+
invalid: [
906+
{
907+
code: "var foo = 'bar';",
908+
filename: 'src\\components\\featureA\\featureA.jsx',
909+
options: [{ '**/*/!(index).*': '<9>' }],
910+
errors: [
911+
{
912+
message:
913+
'The capture group "<9>" is not found in the glob "**/*/!(index).*"',
914+
column: 1,
915+
line: 1,
916+
},
917+
],
918+
},
919+
],
920+
}
921+
);

0 commit comments

Comments
 (0)