Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

[new-rule-option] no-space-keywords for comment-format #4003

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 101 additions & 21 deletions src/rules/commentFormatRule.ts
Expand Up @@ -22,13 +22,18 @@ 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;
}

interface Options {
space: boolean;
noSpaceExceptions?: RegExp;
case: Case;
exceptions?: RegExp;
failureSuffix: string;
Expand All @@ -37,7 +42,7 @@ interface Options {
const enum Case {
None,
Lower,
Upper,
Upper
}

const OPTION_SPACE = "check-space";
Expand Down Expand Up @@ -65,44 +70,64 @@ 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",
items: {
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",
properties: {
"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 */
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -119,7 +144,7 @@ export class Rule extends Lint.Rules.AbstractRule {
}
}

function parseOptions(options: Array<string | IExceptionsObject>): Options {
function parseOptions(options: Array<string | NoSpaceKeywordsObject | IExceptionsObject>): Options {
return {
case:
options.indexOf(OPTION_LOWERCASE) !== -1
Expand All @@ -128,32 +153,73 @@ function parseOptions(options: Array<string | IExceptionsObject>): 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)
};
}

const ignoreWords = option["ignore-words"];
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;
Expand All @@ -179,15 +245,29 @@ function walk(ctx: Lint.WalkContext<Options>) {
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);
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
// Check optional colon after keyword
if (commentText[0] === ":") {
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
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, " ")
]);
}

Expand Down
@@ -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
}
/// <not a reference>
////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
@@ -0,0 +1,5 @@
{
"rules": {
"comment-format": [true, "check-space", {"no-space-keywords": ["TODO", "FIXME"]}, "check-lowercase"]
}
}
34 changes: 34 additions & 0 deletions 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
}
/// <not a reference>
////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
5 changes: 5 additions & 0 deletions 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"]}]
}
}