diff --git a/src/configs/all.ts b/src/configs/all.ts index df3ba9dfe2a..894aacf9087 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -177,6 +177,7 @@ export const rules = { "no-unnecessary-callback-wrapper": true, "no-unnecessary-initializer": true, "no-unnecessary-qualifier": true, + "number-literal-format": true, "object-literal-key-quotes": [true, "consistent-as-needed"], "object-literal-shorthand": true, "one-line": [true, diff --git a/src/rules/numberLiteralFormatRule.ts b/src/rules/numberLiteralFormatRule.ts new file mode 100644 index 00000000000..cf01a59d73c --- /dev/null +++ b/src/rules/numberLiteralFormatRule.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2017 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 { isNumericLiteral } from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; +import { isUpperCase } from "./variableNameRule"; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "number-literal-format", + description: "Checks that decimal literals should begin with '0.' instead of just '.', and should not end with a trailing '0'.", + optionsDescription: "Not configurable.", + options: null, + optionExamples: ["true"], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING_LEADING_0 = "Number literal should not have a leading '0'."; + public static FAILURE_STRING_TRAILING_0 = "Number literal should not have a trailing '0'."; + public static FAILURE_STRING_TRAILING_DECIMAL = "Number literal should not end in '.'."; + public static FAILURE_STRING_LEADING_DECIMAL = "Number literal should begin with '0.' and not just '.'."; + public static FAILURE_STRING_NOT_UPPERCASE = "Hexadecimal number literal should be uppercase."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +function walk(ctx: Lint.WalkContext): void { + const { sourceFile } = ctx; + return ts.forEachChild(sourceFile, function cb(node: ts.Node): void { + if (isNumericLiteral(node)) { + return check(node); + } + return ts.forEachChild(node, cb); + }); + + function check(node: ts.NumericLiteral): void { + // Apparently the number literal '0.0' has a '.text' of '0', so use '.getText()' instead. + const text = node.getText(sourceFile); + + if (text.length <= 1) { + return; + } + + if (text.startsWith("0")) { + // Hex/octal/binary number can't have decimal point or exponent, so no other errors possible. + switch (text[1]) { + case "x": + if (!isUpperCase(text.slice(2))) { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING_NOT_UPPERCASE); + } + return; + case "o": + case "b": + return; + case ".": + break; + default: + ctx.addFailureAtNode(node, Rule.FAILURE_STRING_LEADING_0); + return; + } + } + + const [num, exp] = text.split(/e/i); + if (exp !== undefined && (exp.startsWith("-0") || exp.startsWith("0"))) { + ctx.addFailureAt(node.getEnd() - exp.length, exp.length, Rule.FAILURE_STRING_LEADING_0); + } + + if (!num.includes(".")) { + return; + } + + if (num.startsWith(".")) { + fail(Rule.FAILURE_STRING_LEADING_DECIMAL); + } + + if (num.endsWith(".")) { + fail(Rule.FAILURE_STRING_TRAILING_DECIMAL); + } + + // Allow '10', but not '1.0' + if (num.endsWith("0")) { + fail(Rule.FAILURE_STRING_TRAILING_0); + } + + function fail(message: string): void { + ctx.addFailureAt(node.getStart(sourceFile), num.length, message); + } + } +} diff --git a/test/rules/number-literal-format/test.ts.lint b/test/rules/number-literal-format/test.ts.lint new file mode 100644 index 00000000000..0b6b363e78d --- /dev/null +++ b/test/rules/number-literal-format/test.ts.lint @@ -0,0 +1,40 @@ +0; +0.5; +10; +1.1e10; + +01; +~~ [leading-0] + +1. +~~ [trailing-decimal] + +0.0; +~~~ [trailing-0] +0.50; +~~~~ [trailing-0] + +.5; +~~ [leading-decimal] + +.50; +~~~ [trailing-0] +~~~ [leading-decimal] + +1e01; + ~~ [leading-0] +1E01; + ~~ [leading-0] +1e-01; + ~~~ [leading-0] +1.0e10; +~~~ [trailing-0] + +0xDEAdBEEF; +~~~~~~~~~~ [uppercase] + +[leading-0]: Number literal should not have a leading '0'. +[trailing-0]: Number literal should not have a trailing '0'. +[trailing-decimal]: Number literal should not end in '.'. +[leading-decimal]: Number literal should begin with '0.' and not just '.'. +[uppercase]: Hexadecimal number literal should be uppercase. diff --git a/test/rules/number-literal-format/tslint.json b/test/rules/number-literal-format/tslint.json new file mode 100644 index 00000000000..ec0d152e9f2 --- /dev/null +++ b/test/rules/number-literal-format/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "number-literal-format": true + } +}