diff options
author | Xianny <8582774+xianny@users.noreply.github.com> | 2019-01-12 02:04:30 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-12 02:04:30 +0800 |
commit | 943c378309a84ed142852c6584b53958df7817c8 (patch) | |
tree | 43536b7812b11f3a83db4208bfa5640643cfd6e8 /packages/tslint-config | |
parent | cf3787edbb9e8acf7160ab93b903b54c63bdffda (diff) | |
download | dexon-sol-tools-943c378309a84ed142852c6584b53958df7817c8.tar.gz dexon-sol-tools-943c378309a84ed142852c6584b53958df7817c8.tar.zst dexon-sol-tools-943c378309a84ed142852c6584b53958df7817c8.zip |
Implement tslint enum-naming to enforce PascalCase on enum members (#1474)
Diffstat (limited to 'packages/tslint-config')
-rw-r--r-- | packages/tslint-config/package.json | 1 | ||||
-rw-r--r-- | packages/tslint-config/rules/enumNamingRule.ts | 60 | ||||
-rw-r--r-- | packages/tslint-config/test/enumNamingSpec.spec.ts | 88 | ||||
-rw-r--r-- | packages/tslint-config/test/lintrunner.ts | 23 | ||||
-rw-r--r-- | packages/tslint-config/tsconfig.json | 4 | ||||
-rw-r--r-- | packages/tslint-config/tslint.json | 3 |
6 files changed, 176 insertions, 3 deletions
diff --git a/packages/tslint-config/package.json b/packages/tslint-config/package.json index 77c28f32c..cf39bde3e 100644 --- a/packages/tslint-config/package.json +++ b/packages/tslint-config/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "tsc -b", "build:ci": "yarn build", + "test": "mocha ./lib/test/*.spec.js", "clean": "shx rm -rf lib", "lint": "tslint --format stylish --project ." }, diff --git a/packages/tslint-config/rules/enumNamingRule.ts b/packages/tslint-config/rules/enumNamingRule.ts new file mode 100644 index 000000000..56499618f --- /dev/null +++ b/packages/tslint-config/rules/enumNamingRule.ts @@ -0,0 +1,60 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +export class Rule extends Lint.Rules.AbstractRule { + public static FAILURE_STRING = `Enum member names should be PascalCase`; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(ctx: Lint.WalkContext<void>): void { + // Recursively walk the AST starting with root node, `ctx.sourceFile`. + // Call the function `cb` (defined below) for each child. + return ts.forEachChild(ctx.sourceFile, cb); + + function cb(node: ts.Node): void { + if (node.kind === ts.SyntaxKind.EnumMember) { + const keyNode = node.getFirstToken(ctx.sourceFile); + if (keyNode !== undefined) { + const keyText = keyNode.getText(ctx.sourceFile); + if (!isPascalCase(keyText)) { + return ctx.addFailureAtNode(node, Rule.FAILURE_STRING, getFix(keyText, node)); + } + } + } + // Continue recursion into the AST by calling function `cb` for every child of the current node. + return ts.forEachChild(node, cb); + } + + function getFix(text: string, node: ts.Node): Lint.Replacement { + let fix = toPascalCase(text); + // check for `member = value` + if (node.getChildCount(ctx.sourceFile) === 3) { + const value = node.getLastToken(ctx.sourceFile); + if (value !== undefined) { + fix += ` = ${value.getText(ctx.sourceFile)}`; + } + } + return new Lint.Replacement(node.getStart(ctx.sourceFile), node.getWidth(ctx.sourceFile), fix); + } +} + +// Modified from: https://github.com/jonschlinkert/pascalcase/ +function toPascalCase(str: string): string { + let result = str.replace(/([a-z0-9\W])([A-Z])/g, '$1 $2'); + if (result.length === 1) { + return result.toUpperCase(); + } + result = result.replace(/^[\W_\.]+|[\W_\.]+$/g, '').toLowerCase(); + result = result.charAt(0).toUpperCase() + result.slice(1); + return result.replace(/[\W_\.]+(\w|$)/g, (_, ch) => { + return ch.toUpperCase(); + }); +} +function isPascalCase(s: string): boolean { + const regex = /^([A-Z0-9]+[a-z0-9]+)+$/g; + const key = s.split('=')[0].trim(); + return regex.test(key); +} diff --git a/packages/tslint-config/test/enumNamingSpec.spec.ts b/packages/tslint-config/test/enumNamingSpec.spec.ts new file mode 100644 index 000000000..d5b864eba --- /dev/null +++ b/packages/tslint-config/test/enumNamingSpec.spec.ts @@ -0,0 +1,88 @@ +import * as assert from 'assert'; + +import { Rule } from '../rules/enumNamingRule'; + +import { getFixedResult, helper } from './lintrunner'; +const rule = 'enum-naming'; + +describe('enumNamingRule', () => { + it(`should not fail PascalCase`, () => { + const src = `enum test { MemberOne, MemberTwo }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 0); + }); + it(`should not fail PascalCase keys with uncased values`, () => { + const src = `enum test { MemberOne = 'member_one', MemberTwo = 'member two' }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 0); + }); + it(`should not fail PascalCase keys with numbers`, () => { + const src = `enum test { Member1 = 'member_one', MemberTwo = 'member two' }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 0); + }); + it(`should fail with camelCase`, () => { + const src = `enum test { memberOne, memberTwo }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 2); + }); + it(`should fail with snake case`, () => { + const src = `enum test { member_one, member_two }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 2); + }); + it(`should fail with all caps`, () => { + const src = `enum test { MEMBERONE, MEMBER_TWO }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 2); + }); + it(`should fail with mixed case`, () => { + const src = `enum test { member_one, MemberTwo }`; + const result = helper(src, rule); + assert.equal(result.errorCount, 1); + }); + + it(`should fail with the right position`, () => { + const src = `enum test { MemberOne, member_two }`; + const startPosition = src.indexOf('member_two'); + const endPosition = startPosition + 'member_two'.length; + const failure = helper(src, rule).failures[0]; + + assert.equal(failure.getStartPosition().getPosition(), startPosition); + assert.equal(failure.getEndPosition().getPosition(), endPosition); + assert.equal(failure.getFailure(), Rule.FAILURE_STRING); + }); + + it(`should fail with the right message`, () => { + const src = `enum test { memberOne, memberTwo }`; + const failure = helper(src, rule).failures[0]; + + assert.equal(failure.getFailure(), Rule.FAILURE_STRING); + }); +}); +describe('enumNaming fixer', () => { + it('should fix keys', () => { + const src = `enum test { MemberOne, memberTwo, member_three, MEMBER_FOUR, MEMBERFIVE }`; + const expected = `enum test { MemberOne, MemberTwo, MemberThree, MemberFour, Memberfive }`; + const actual = getFixedResult(src, rule); + const result = helper(src, rule); + assert.equal(result.errorCount, 4); // tslint:disable-line:custom-no-magic-numbers + assert.equal(actual, expected); + }); + it('should not fix values', () => { + const src = `enum test { MemberOne = 'MemberOne', memberTwo = 'memberTwo', member_three = 'member_three', MEMBER_FOUR = 'MEMBER_FOUR' }`; + const expected = `enum test { MemberOne = 'MemberOne', MemberTwo = 'memberTwo', MemberThree = 'member_three', MemberFour = 'MEMBER_FOUR' }`; + const actual = getFixedResult(src, rule); + const result = helper(src, rule); + assert.equal(result.errorCount, 3); // tslint:disable-line:custom-no-magic-numbers + assert.equal(actual, expected); + }); + it('should preserve values with equals sign', () => { + const src = `enum Operators { assign = '=', EQUALS = '==', Triple_Equals = '===' }`; + const expected = `enum Operators { Assign = '=', Equals = '==', TripleEquals = '===' }`; + const actual = getFixedResult(src, rule); + const result = helper(src, rule); + assert.equal(result.errorCount, 3); // tslint:disable-line:custom-no-magic-numbers + assert.equal(actual, expected); + }); +}); diff --git a/packages/tslint-config/test/lintrunner.ts b/packages/tslint-config/test/lintrunner.ts new file mode 100644 index 000000000..fcd1b6844 --- /dev/null +++ b/packages/tslint-config/test/lintrunner.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import { Configuration, Linter, Replacement } from 'tslint'; + +export const helper = (src: string, rule: string) => { + const linter = new Linter({ fix: false }); + linter.lint( + '', + src, + Configuration.parseConfigFile({ + rules: { + [rule]: true, + }, + rulesDirectory: path.join(__dirname, '../rules'), + }), + ); + return linter.getResult(); +}; + +export const getFixedResult = (src: string, rule: string) => { + const result = helper(src, rule); + const fixes = [].concat.apply(result.failures.map(x => x.getFix())); + return Replacement.applyFixes(src, fixes); +}; diff --git a/packages/tslint-config/tsconfig.json b/packages/tslint-config/tsconfig.json index 44845cf1f..b9a4dd03e 100644 --- a/packages/tslint-config/tsconfig.json +++ b/packages/tslint-config/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig", "compilerOptions": { "outDir": "lib", - "rootDir": "rules" + "rootDir": "." }, - "include": ["./rules/**/*"] + "include": ["./rules/**/*", "test/**/*"] } diff --git a/packages/tslint-config/tslint.json b/packages/tslint-config/tslint.json index e8de6221e..a5fa6962c 100644 --- a/packages/tslint-config/tslint.json +++ b/packages/tslint-config/tslint.json @@ -25,6 +25,7 @@ "curly": true, "custom-no-magic-numbers": [true, 0, 1, 2, 3, -1], "encoding": true, + "enum-naming": true, "eofline": true, "import-spacing": true, "indent": [true, "spaces", 4], @@ -125,5 +126,5 @@ "check-preblock" ] }, - "rulesDirectory": "lib" + "rulesDirectory": "lib/rules" } |