Added possibility to enforce different file name casings #4206
Changes from 2 commits
4f57de1
cf74d36
d40b082
96d92db
de96576
59f16da
da5755e
feb268d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,47 +25,95 @@ enum Casing { | |
CamelCase = "camel-case", | ||
PascalCase = "pascal-case", | ||
KebabCase = "kebab-case", | ||
SnakeCase = "snake-case", | ||
SnakeCase = "snake-case" | ||
} | ||
|
||
type FileNameRegExpWithCasing = [string, Casing]; | ||
type FileNameCasings = FileNameRegExpWithCasing[]; | ||
|
||
export class Rule extends Lint.Rules.AbstractRule { | ||
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: "file-name-casing", | ||
description: "Enforces a consistent file naming convention", | ||
rationale: "Helps maintain a consistent style across a file hierarchy", | ||
optionsDescription: Lint.Utils.dedent` | ||
One of the following arguments must be provided: | ||
One argument which is either a string defining the file casing or an array consisting of a file name | ||
matches and corresponding casing strings. | ||
|
||
* \`${Casing.CamelCase}\`: File names must be camel-cased: \`fileName.ts\`. | ||
* \`${Casing.PascalCase}\`: File names must be Pascal-cased: \`FileName.ts\`. | ||
* \`${Casing.KebabCase}\`: File names must be kebab-cased: \`file-name.ts\`. | ||
* \`${Casing.SnakeCase}\`: File names must be snake-cased: \`file_name.ts\`.`, | ||
* In both cases the casing string must be one of the options: | ||
** \`${Casing.CamelCase}\`: File names must be camel-cased: \`fileName.ts\`. | ||
** \`${Casing.PascalCase}\`: File names must be pascal-cased: \`FileName.ts\`. | ||
** \`${Casing.KebabCase}\`: File names must be kebab-cased: \`file-name.ts\`. | ||
** \`${Casing.SnakeCase}\`: File names must be snake-cased: \`file_name.ts\`. | ||
|
||
* The array again consists of array with two items. The first item must be a case-insenstive | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Should this be updated to mention that it takes in an object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, forgot that, thanks. I changed it now but since English isn't my first language feel free to point out anything that could be phrased better! |
||
regular expression to match files, the second item must be a valid casing option (see above)`, | ||
options: { | ||
type: "array", | ||
items: [ | ||
{ | ||
type: "string", | ||
enum: [Casing.CamelCase, Casing.PascalCase, Casing.KebabCase, Casing.SnakeCase], | ||
}, | ||
], | ||
type: "list", | ||
listType: { | ||
anyOf: [ | ||
{ | ||
type: "string", | ||
enum: [ | ||
Casing.CamelCase, | ||
Casing.PascalCase, | ||
Casing.KebabCase, | ||
Casing.SnakeCase | ||
] | ||
}, | ||
{ | ||
type: "array", | ||
items: [ | ||
{ | ||
type: "string" | ||
}, | ||
{ | ||
type: "string", | ||
enum: [ | ||
Casing.CamelCase, | ||
Casing.PascalCase, | ||
Casing.KebabCase, | ||
Casing.SnakeCase | ||
] | ||
} | ||
] | ||
} | ||
] | ||
} | ||
}, | ||
optionExamples: [ | ||
[true, Casing.CamelCase], | ||
[true, Casing.PascalCase], | ||
[true, Casing.KebabCase], | ||
[true, Casing.SnakeCase], | ||
[ | ||
true, | ||
[ | ||
[".style.ts$", Casing.KebabCase], | ||
[".tsx$", Casing.PascalCase], | ||
[".*", Casing.CamelCase] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's generally convention to use objects for key/value pairs. Unless you have a specific need, please switch to something like: {
".style.ts$": Casing.KebabCase,
".tsx$": Casing.PascalCase,
".*": Casing.CamelCase,
} One benefit, in addition to being semantically more clear, is that you get rid of duplicate keys easily. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will switch to object, thanks! |
||
] | ||
] | ||
], | ||
hasFix: false, | ||
type: "style", | ||
typescriptOnly: false, | ||
typescriptOnly: false | ||
}; | ||
/* tslint:enable:object-literal-sort-keys */ | ||
|
||
private static FAILURE_STRING(expectedCasing: Casing): string { | ||
return `File name must be ${Rule.stylizedNameForCasing(expectedCasing)}`; | ||
} | ||
|
||
private static isValidCasingOption(casing: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: instead of this dedicated function, you can keep a |
||
return ( | ||
[Casing.CamelCase, Casing.KebabCase, Casing.PascalCase, Casing.SnakeCase].indexOf( | ||
casing as Casing | ||
) !== -1 | ||
); | ||
} | ||
|
||
private static stylizedNameForCasing(casing: Casing): string { | ||
switch (casing) { | ||
case Casing.CamelCase: | ||
|
@@ -79,7 +127,7 @@ export class Rule extends Lint.Rules.AbstractRule { | |
} | ||
} | ||
|
||
private static isCorrectCasing(fileName: string, casing: Casing): boolean { | ||
private static hasFileNameCorrectCasing(fileName: string, casing: Casing): boolean { | ||
switch (casing) { | ||
case Casing.CamelCase: | ||
return isCamelCased(fileName); | ||
|
@@ -92,15 +140,53 @@ export class Rule extends Lint.Rules.AbstractRule { | |
} | ||
} | ||
|
||
private static isValidRegExp(regExpString: string) { | ||
try { | ||
RegExp(regExpString); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
|
||
private static findApplicableCasing( | ||
fileBaseName: string, | ||
fileNameCasings: FileNameCasings | ||
): Casing | null { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why so many Unless you have a real need for using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad, in my team I'm still fighting for |
||
const applicableCasing = fileNameCasings.find(fileNameCasing => { | ||
const fileNameMatch = fileNameCasing[0]; | ||
return ( | ||
Rule.isValidRegExp(fileNameMatch) && RegExp(fileNameMatch, "i").test(fileBaseName) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...so if an invalid regular expression is provided, the rule will fail silently? We should prefer to fail verbosely for invalid configurations. Either remove the There are a lot of ways to provide invalid configurations, and it's near-impossible to guard against all of them. TSLint is moving towards standardized config options but it'll be a while before they're ready. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, there already were cases like this in the current implementation, see here for example: if the mandatory casing wasn't provided simply return an empty array 🤷♂️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JoshuaKGoldberg I just realized that it might be not a good idea to either throw an error or use console.warn, since this would happen for every linted file in the project. Using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Filed #4280 to tackle this more broadly. In the meantime, feel free to either use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the issue! (There's a small typo in my mention 😌) I now added some uses of |
||
); | ||
}); | ||
|
||
return applicableCasing !== undefined ? applicableCasing[1] : null; | ||
} | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
if (this.ruleArguments.length !== 1) { | ||
return []; | ||
} | ||
|
||
const casing = this.ruleArguments[0] as Casing; | ||
const fileName = path.parse(sourceFile.fileName).name; | ||
if (!Rule.isCorrectCasing(fileName, casing)) { | ||
return [new Lint.RuleFailure(sourceFile, 0, 0, Rule.FAILURE_STRING(casing), this.ruleName)]; | ||
let casing: Casing | null = null; | ||
|
||
const parsedPath = path.parse(sourceFile.fileName); | ||
|
||
if (typeof this.ruleArguments[0] === "object") { | ||
casing = Rule.findApplicableCasing(parsedPath.base, this | ||
.ruleArguments[0] as FileNameCasings); | ||
} else if (typeof this.ruleArguments[0] === "string") { | ||
casing = this.ruleArguments[0] as Casing; | ||
} | ||
|
||
if (casing === null || !Rule.isValidCasingOption(casing)) { | ||
return []; | ||
} | ||
|
||
if (!Rule.hasFileNameCorrectCasing(parsedPath.name, casing)) { | ||
return [ | ||
new Lint.RuleFailure(sourceFile, 0, 0, Rule.FAILURE_STRING(casing), this.ruleName) | ||
]; | ||
} | ||
|
||
return []; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"file-name-casing": [true, "camel-case"] | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
~nil [File name must be PascalCase] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
~nil [File name must be camelCase] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
~nil [File name must be kebab-case] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"rules": { | ||
"file-name-casing": [ | ||
true, | ||
[ | ||
[".*)", "snake-case"], // making sure invalid RegExp will be ignored | ||
["pascal.?case", "pascal-case"], | ||
["tsx$", "kebab-case"], | ||
[".*", "camel-case"] | ||
] | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"file-name-casing": [true, "invalid-option"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
~nil [File name must be kebab-case] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"file-name-casing": [true, "kebab-case"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
~nil [File name must be PascalCase] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"file-name-casing": [true, "pascal-case"] | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, mind taking these changes out into a separate PR? Definitely a bug in
launch.json
that the file hasn't been renamed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't mind at all, it's better to separate those things. Reverted my changes and will open a new PR for that fix.