Skip to content

Commit

Permalink
[New rule] add fileNameCasing rule (palantir#3978)
Browse files Browse the repository at this point in the history
* add fileNameCasing rule

* use camel case name for tslintCli

* refactor casing check into helper function

* use stylized name for casing in failure message
  • Loading branch information
euclio authored and giladgray committed Jun 26, 2018
1 parent d95dd91 commit 1c31dc0
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 6 deletions.
2 changes: 1 addition & 1 deletion bin/tslint
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node

require("../lib/tslint-cli");
require("../lib/tslintCli");
1 change: 1 addition & 0 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export const rules = {
// "file-header": No sensible default
"deprecation": true,
"encoding": true,
"file-name-casing": [true, "camel-case"],
"import-spacing": true,
"interface-name": true,
"interface-over-type-literal": true,
Expand Down
6 changes: 1 addition & 5 deletions src/rules/classNameRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { isClassLikeDeclaration, isInterfaceDeclaration } from "tsutils";
import * as ts from "typescript";

import * as Lint from "../index";
import { isUpperCase } from "../utils";
import { isPascalCased } from "../utils";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
Expand Down Expand Up @@ -58,7 +58,3 @@ function walk(ctx: Lint.WalkContext<void>) {
return ts.forEachChild(node, cb);
});
}

function isPascalCased(name: string): boolean {
return isUpperCase(name[0]) && !name.includes("_");
}
101 changes: 101 additions & 0 deletions src/rules/fileNameCasingRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @license
* Copyright 2018 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as path from "path";
import * as ts from "typescript";

import * as Lint from "../index";
import { isCamelCased, isKebabCased, isPascalCased } from "../utils";

enum Casing {
CamelCase = "camel-case",
PascalCase = "pascal-case",
KebabCase = "kebab-case",
}

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:
* \`${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\`.`,
options: {
type: "array",
items: [
{
type: "string",
enum: [Casing.CamelCase, Casing.PascalCase, Casing.KebabCase],
},
],
},
optionExamples: [
[true, Casing.CamelCase],
[true, Casing.PascalCase],
[true, Casing.KebabCase],
],
hasFix: false,
type: "style",
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 stylizedNameForCasing(casing: Casing): string {
switch (casing) {
case Casing.CamelCase:
return "camelCase";
case Casing.PascalCase:
return "PascalCase";
case Casing.KebabCase:
return "kebab-case";
}
}

private static isCorrectCasing(fileName: string, casing: Casing): boolean {
switch (casing) {
case Casing.CamelCase:
return isCamelCased(fileName);
case Casing.PascalCase:
return isPascalCased(fileName);
case Casing.KebabCase:
return isKebabCased(fileName);
}
}

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)];
}

return [];
}
}
File renamed without changes.
18 changes: 18 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,21 @@ export function detectBufferEncoding(buffer: Buffer, length = buffer.length): En
export function denormalizeWinPath(path: string): string {
return path.replace(/\\/g, "/");
}

export function isPascalCased(name: string): boolean {
return isUpperCase(name[0]) && !name.includes("_") && !name.includes("-");
}

export function isCamelCased(name: string): boolean {
return isLowerCase(name[0]) && !name.includes("_") && !name.includes("-");
}

export function isKebabCased(name: string): boolean {
for (let i = 0; i < name.length; i++) {
const c = name.charAt(i);
if (c === "_" || !isLowerCase(c)) {
return false;
}
}
return true;
}
Empty file.
2 changes: 2 additions & 0 deletions test/rules/file-name-casing/default/no-camel-case.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

~nil [File name must be camelCase]
5 changes: 5 additions & 0 deletions test/rules/file-name-casing/default/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"file-name-casing": [true, "camel-case"]
}
}

0 comments on commit 1c31dc0

Please sign in to comment.