diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts index d271dd779c3..8be72e4d288 100644 --- a/src/rules/commentFormatRule.ts +++ b/src/rules/commentFormatRule.ts @@ -22,6 +22,10 @@ import { ENABLE_DISABLE_REGEX } from "../enableDisableRules"; import * as Lint from "../index"; import { escapeRegExp, isLowerCase, isUpperCase } from "../utils"; +interface NoSpaceKeywordsObject { + "no-space-keywords": string[]; +} + interface IExceptionsObject { "ignore-words"?: string[]; "ignore-pattern"?: string; @@ -29,6 +33,7 @@ interface IExceptionsObject { interface Options { space: boolean; + noSpaceExceptions?: RegExp; case: Case; exceptions?: RegExp; failureSuffix: string; @@ -37,7 +42,7 @@ interface Options { const enum Case { None, Lower, - Upper, + Upper } const OPTION_SPACE = "check-space"; @@ -65,6 +70,12 @@ export class Rule extends Lint.Rules.AbstractRule { * \`"ignore-words"\` - array of strings - words that will be ignored at the beginning of the comment. * \`"ignore-pattern"\` - string - RegExp pattern that will be ignored at the beginning of the comment. + + Keywords can be managed with an object that can be passed as a second argument (right after \`"check-space"\`). + + One option can be provided in this object: + + * \`"no-space-keywords"\` - array of strings - keywords that can be used at the beginning of the comment. `, options: { type: "array", @@ -72,7 +83,20 @@ export class Rule extends Lint.Rules.AbstractRule { anyOf: [ { type: "string", - enum: ["check-space", "check-lowercase", "check-uppercase"], + enum: ["check-space", "check-lowercase", "check-uppercase"] + }, + { + type: "object", + properties: { + "no-space-keywords": { + type: "array", + items: { + type: "string" + } + } + }, + minProperties: 1, + maxProperties: 1 }, { type: "object", @@ -80,29 +104,30 @@ export class Rule extends Lint.Rules.AbstractRule { "ignore-words": { type: "array", items: { - type: "string", - }, + type: "string" + } }, "ignore-pattern": { - type: "string", - }, + type: "string" + } }, minProperties: 1, - maxProperties: 1, - }, - ], + maxProperties: 1 + } + ] }, minLength: 1, - maxLength: 4, + maxLength: 5 }, optionExamples: [ [true, "check-space", "check-uppercase"], + [true, "check-space", { "no-space-keywords": ["TODO", "FIXME"] }], [true, "check-lowercase", { "ignore-words": ["TODO", "HACK"] }], - [true, "check-lowercase", { "ignore-pattern": "STD\\w{2,3}\\b" }], + [true, "check-lowercase", { "ignore-pattern": "STD\\w{2,3}\\b" }] ], type: "style", typescriptOnly: false, - hasFix: true, + hasFix: true }; /* tslint:enable:object-literal-sort-keys */ @@ -119,7 +144,7 @@ export class Rule extends Lint.Rules.AbstractRule { } } -function parseOptions(options: Array): Options { +function parseOptions(options: Array): Options { return { case: options.indexOf(OPTION_LOWERCASE) !== -1 @@ -128,22 +153,63 @@ function parseOptions(options: Array): Options { ? Case.Upper : Case.None, failureSuffix: "", + noSpaceExceptions: parseNoSpaceKeywords(options[1]), space: options.indexOf(OPTION_SPACE) !== -1, - ...composeExceptions(options[options.length - 1]), + ...composeExceptions(options[options.length - 1]) }; } +function isNoSpaceKeywordsObject( + option?: string | NoSpaceKeywordsObject | IExceptionsObject +): option is NoSpaceKeywordsObject { + if (typeof option !== "object") { + return false; + } + if (!("no-space-keywords" in option)) { + return false; + } + if (!Array.isArray(option["no-space-keywords"])) { + return false; + } + return true; +} + +function parseNoSpaceKeywords( + option?: string | NoSpaceKeywordsObject | IExceptionsObject +): RegExp | undefined { + if (!isNoSpaceKeywordsObject(option)) { + return undefined; + } + const keywords = option["no-space-keywords"]; + return new RegExp(`^(${keywords.map(keyword => escapeRegExp(keyword.trim())).join("|")})\\b`); +} + +function isIExceptionsObject( + option?: string | NoSpaceKeywordsObject | IExceptionsObject +): option is IExceptionsObject { + if (typeof option !== "object") { + return false; + } + if (!("ignore-pattern" in option) && !("ignore-words" in option)) { + return false; + } + if (!(typeof option["ignore-pattern"] === "string") && !Array.isArray(option["ignore-words"])) { + return false; + } + return true; +} + function composeExceptions( - option?: string | IExceptionsObject, + option?: string | NoSpaceKeywordsObject | IExceptionsObject ): undefined | { exceptions: RegExp; failureSuffix: string } { - if (typeof option !== "object") { + if (!isIExceptionsObject(option)) { return undefined; } const ignorePattern = option["ignore-pattern"]; if (ignorePattern !== undefined) { return { exceptions: new RegExp(`^\\s*(${ignorePattern})`), - failureSuffix: Rule.IGNORE_PATTERN_FAILURE_FACTORY(ignorePattern), + failureSuffix: Rule.IGNORE_PATTERN_FAILURE_FACTORY(ignorePattern) }; } @@ -151,9 +217,9 @@ function composeExceptions( if (ignoreWords !== undefined && ignoreWords.length !== 0) { return { exceptions: new RegExp( - `^\\s*(?:${ignoreWords.map(word => escapeRegExp(word.trim())).join("|")})\\b`, + `^\\s*(?:${ignoreWords.map(word => escapeRegExp(word.trim())).join("|")})\\b` ), - failureSuffix: Rule.IGNORE_WORDS_FAILURE_FACTORY(ignoreWords), + failureSuffix: Rule.IGNORE_WORDS_FAILURE_FACTORY(ignoreWords) }; } return undefined; @@ -179,15 +245,29 @@ function walk(ctx: Lint.WalkContext) { if (start === end) { return; } - const commentText = fullText.slice(start, end); + let commentText = fullText.slice(start, end); // whitelist //#region and //#endregion and JetBrains IDEs' "//noinspection ..." if (/^(?:#(?:end)?region|noinspection\s)/.test(commentText)) { return; } + if (ctx.options.noSpaceExceptions !== undefined) { + const match = ctx.options.noSpaceExceptions.exec(commentText); + if (match !== null) { + const lengthOfMatch = match[0].length; + commentText = commentText.substring(lengthOfMatch); + // Check optional colon after keyword + if (commentText[0] === ":") { + commentText = commentText.substring(1); + ++start; + } + start += lengthOfMatch; + } + } + if (ctx.options.space && commentText[0] !== " ") { ctx.addFailure(start, end, Rule.LEADING_SPACE_FAILURE, [ - Lint.Replacement.appendText(start, " "), + Lint.Replacement.appendText(start, " ") ]); } diff --git a/test/rules/comment-format/ignore-space-on-keyword-and-lower/test.ts.lint b/test/rules/comment-format/ignore-space-on-keyword-and-lower/test.ts.lint new file mode 100644 index 00000000000..14d3cd1c1b0 --- /dev/null +++ b/test/rules/comment-format/ignore-space-on-keyword-and-lower/test.ts.lint @@ -0,0 +1,51 @@ +class Clazz { // this comment is correct + /* block comment + * adada + */ + public funcxion() { // This comment has a capital letter starting it + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + //This comment is on its own line, and starts with a capital _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [lower] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test"); //this comment has no space + ~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test2"); //TODO: this comment has no space but TODO + } + /// + ////foo + ~~~ [space] +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +//noinspection JSUnusedGlobalSymbols +const unusedVar = 'unneeded value'; + +//normal comment + ~~~~~~~~~~~~~~ [space] + +// normal comment + +//TODO: todo description + +//TODO: Todo description + ~~~~~~~~~~~~~~~~~ [lower] + +//FIXME: todo description + +//FIXME:todo description + ~~~~~~~~~~~~~~~~ [space] + +//HACK: unknown keyword + ~~~~~~~~~~~~~~~~~~~~~ [lower] + ~~~~~~~~~~~~~~~~~~~~~ [space] + +//HACK: Unknown keyword + ~~~~~~~~~~~~~~~~~~~~~ [lower] + ~~~~~~~~~~~~~~~~~~~~~ [space] + +[lower]: comment must start with lowercase letter +[space]: comment must start with a space diff --git a/test/rules/comment-format/ignore-space-on-keyword-and-lower/tslint.json b/test/rules/comment-format/ignore-space-on-keyword-and-lower/tslint.json new file mode 100644 index 00000000000..26326504388 --- /dev/null +++ b/test/rules/comment-format/ignore-space-on-keyword-and-lower/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "comment-format": [true, "check-space", {"no-space-keywords": ["TODO", "FIXME"]}, "check-lowercase"] + } +} diff --git a/test/rules/comment-format/ignore-space-on-keyword/test.ts.lint b/test/rules/comment-format/ignore-space-on-keyword/test.ts.lint new file mode 100644 index 00000000000..19bfd92ad2f --- /dev/null +++ b/test/rules/comment-format/ignore-space-on-keyword/test.ts.lint @@ -0,0 +1,34 @@ +class Clazz { // this comment is correct + /* block comment + * adada + */ + public funcxion() { // This comment has a capital letter starting it + //This comment is on its own line, and starts with a capital _and_ no space + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test"); //this comment has no space + ~~~~~~~~~~~~~~~~~~~~~~~~~ [space] + console.log("test2"); //TODO: this comment has no space but TODO + } + /// + ////foo + ~~~ [space] +} + +//#region test +//#endregion + +`${location.protocol}//${location.hostname}` + +//normal comment + ~~~~~~~~~~~~~~ [space] + +// normal comment + +//TODO: todo description + +//FIXME: todo description + +//HACK: unknown keyword + ~~~~~~~~~~~~~~~~~~~~~ [space] + +[space]: comment must start with a space diff --git a/test/rules/comment-format/ignore-space-on-keyword/tslint.json b/test/rules/comment-format/ignore-space-on-keyword/tslint.json new file mode 100644 index 00000000000..2e8edbd9817 --- /dev/null +++ b/test/rules/comment-format/ignore-space-on-keyword/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "comment-format": [true, "check-space", {"no-space-keywords": ["TODO", "FIXME"]}] + } +}