Added possibility to enforce different file name casings #4206
Changes from 5 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 |
---|---|---|
|
@@ -18,14 +18,15 @@ | |
import * as path from "path"; | ||
import * as ts from "typescript"; | ||
|
||
import { showWarningOnce } from "../error"; | ||
import * as Lint from "../index"; | ||
import { isCamelCased, isKebabCased, isPascalCased, isSnakeCased } from "../utils"; | ||
|
||
enum Casing { | ||
CamelCase = "camel-case", | ||
PascalCase = "pascal-case", | ||
KebabCase = "kebab-case", | ||
SnakeCase = "snake-case", | ||
SnakeCase = "snake-case" | ||
} | ||
|
||
export class Rule extends Lint.Rules.AbstractRule { | ||
|
@@ -35,37 +36,73 @@ export class Rule extends Lint.Rules.AbstractRule { | |
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: [ | ||
oneOf: [ | ||
{ | ||
type: "string", | ||
enum: [Casing.CamelCase, Casing.PascalCase, Casing.KebabCase, Casing.SnakeCase], | ||
enum: [Casing.CamelCase, Casing.PascalCase, Casing.KebabCase, Casing.SnakeCase] | ||
}, | ||
], | ||
{ | ||
type: "object", | ||
additionalProperties: { | ||
type: "string", | ||
enum: [ | ||
Casing.CamelCase, | ||
Casing.PascalCase, | ||
Casing.KebabCase, | ||
Casing.SnakeCase | ||
] | ||
}, | ||
minProperties: 1 | ||
} | ||
] | ||
}, | ||
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 readonly validCasingOptions = new Set([ | ||
Casing.CamelCase, | ||
Casing.KebabCase, | ||
Casing.PascalCase, | ||
Casing.SnakeCase | ||
]); | ||
|
||
private static FAILURE_STRING(expectedCasing: Casing): string { | ||
return `File name must be ${Rule.stylizedNameForCasing(expectedCasing)}`; | ||
} | ||
|
||
private static isValidCasingOption(casing: string): boolean { | ||
return Rule.validCasingOptions.has(casing as Casing); | ||
} | ||
|
||
private static stylizedNameForCasing(casing: Casing): string { | ||
switch (casing) { | ||
case Casing.CamelCase: | ||
|
@@ -79,7 +116,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,16 +129,89 @@ export class Rule extends Lint.Rules.AbstractRule { | |
} | ||
} | ||
|
||
private static isValidRegExp(regExpString: string) { | ||
try { | ||
RegExp(regExpString); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
|
||
private static getCasingFromStringArgument(ruleArgument: string): Casing | undefined { | ||
if (!Rule.isValidCasingOption(ruleArgument)) { | ||
Rule.showWarning(`Unexpected casing option provided: ${ruleArgument}`); | ||
return undefined; | ||
} | ||
|
||
return ruleArgument as Casing; | ||
} | ||
|
||
private static getCasingFromObjectArgument( | ||
ruleArgument: { [index: string]: string }, | ||
fileBaseName: string | ||
): Casing | undefined { | ||
const fileNameMatches = Object.keys(ruleArgument); | ||
|
||
if (fileNameMatches.length === 0) { | ||
Rule.showWarning(`Atleast one file name match must be provided`); | ||
return undefined; | ||
} | ||
|
||
const matchingFileNameMatch = fileNameMatches.find(fileNameMatch => { | ||
if (!this.isValidRegExp(fileNameMatch)) { | ||
Rule.showWarning(`Invalid regular expression provided: ${fileNameMatch}`); | ||
return false; | ||
} | ||
|
||
if (!Rule.isValidCasingOption(ruleArgument[fileNameMatch])) { | ||
Rule.showWarning(`Unexpected casing option provided: ${ruleArgument}`); | ||
return false; | ||
} | ||
|
||
return RegExp(fileNameMatch, "i").test(fileBaseName); | ||
}); | ||
|
||
if (matchingFileNameMatch === undefined) { | ||
return undefined; | ||
} | ||
|
||
return ruleArgument[matchingFileNameMatch] as Casing; | ||
} | ||
|
||
private static showWarning(message: string): void { | ||
showWarningOnce(`Warning: ${Rule.metadata.ruleName} - ${message}`); | ||
} | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
if (this.ruleArguments.length !== 1) { | ||
Rule.showWarning("Provide a rule option as string or object"); | ||
return []; | ||
} | ||
|
||
const ruleArgument = this.ruleArguments[0]; | ||
|
||
let casing: Casing | undefined; | ||
|
||
const parsedPath = path.parse(sourceFile.fileName); | ||
|
||
if (typeof ruleArgument === "object") { | ||
const objectRuleArgument = ruleArgument as { [index: string]: string }; | ||
casing = Rule.getCasingFromObjectArgument(objectRuleArgument, parsedPath.base); | ||
} else if (typeof ruleArgument === "string") { | ||
casing = Rule.getCasingFromStringArgument(ruleArgument); | ||
} else { | ||
Rule.showWarning("Received unexpected rule option"); | ||
return []; | ||
} | ||
|
||
if (casing === undefined) { | ||
return []; | ||
} | ||
|
||
const casing = this.ruleArguments[0] as Casing; | ||
const fileName = path.parse(sourceFile.fileName).name; | ||
if (!Rule.isCorrectCasing(fileName, casing)) { | ||
if (!Rule.hasFileNameCorrectCasing(parsedPath.name, casing)) { | ||
return [ | ||
new Lint.RuleFailure(sourceFile, 0, 0, Rule.FAILURE_STRING(casing), this.ruleName), | ||
new Lint.RuleFailure(sourceFile, 0, 0, Rule.FAILURE_STRING(casing), this.ruleName) | ||
]; | ||
} | ||
|
||
|
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.