From caad563c33d7312d7d9d413cb8d9315ed9f49690 Mon Sep 17 00:00:00 2001 From: Roman Rogowski Date: Thu, 21 Mar 2019 19:25:33 -0400 Subject: [PATCH] [new-rule-option] allow-single-line-comments for file-header rule (#4560) fixes #3180 --- src/rules/fileHeaderRule.ts | 136 +++++++++++++++--- test/rules/file-header/bad/test2.ts.fix | 7 + test/rules/file-header/bad/test2.ts.lint | 4 + .../test.ts.lint | 4 + .../tslint.json | 8 ++ 5 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 test/rules/file-header/bad/test2.ts.fix create mode 100644 test/rules/file-header/bad/test2.ts.lint create mode 100644 test/rules/file-header/good-allow-single-line-comments/test.ts.lint create mode 100644 test/rules/file-header/good-allow-single-line-comments/tslint.json diff --git a/src/rules/fileHeaderRule.ts b/src/rules/fileHeaderRule.ts index 03d44d342ff..764404d3f44 100644 --- a/src/rules/fileHeaderRule.ts +++ b/src/rules/fileHeaderRule.ts @@ -19,7 +19,17 @@ import * as ts from "typescript"; import * as Lint from "../index"; -const ENFORCE_TRAILING_NEWLINE = "enforce-trailing-newline"; +const OPTION_MATCH = "match"; +const OPTION_ALLOW_SINGLE_LINE_COMMENTS = "allow-single-line-comments"; +const OPTION_DEFAULT = "default"; +const OPTION_ENFORCE_TRAILING_NEWLINE = "enforce-trailing-newline"; + +interface FileHeaderRuleOptions { + [OPTION_MATCH]: string; + [OPTION_ALLOW_SINGLE_LINE_COMMENTS]?: boolean; + [OPTION_DEFAULT]?: string; + [OPTION_ENFORCE_TRAILING_NEWLINE]?: boolean; +} export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -28,29 +38,76 @@ export class Rule extends Lint.Rules.AbstractRule { description: "Enforces a certain header comment for all files, matched by a regular expression.", optionsDescription: Lint.Utils.dedent` + A single object may be passed in for configuration that must contain: + + * \`${OPTION_MATCH}\`: a regular expression that all headers should match + + Any of the following optional fields may also be provided: + + * \`${OPTION_ALLOW_SINGLE_LINE_COMMENTS}\`: a boolean for whether \`//\` should be considered file headers in addition to \`/*\` comments + * \`${OPTION_DEFAULT}\`: text to add for file headers when running in \`--fix\` mode + * \`${OPTION_ENFORCE_TRAILING_NEWLINE}\`: a boolean for whether a newline must follow the header + + The rule will also accept array of strings as a legacy form of options, though the object form is recommended. The first option, which is mandatory, is a regular expression that all headers should match. The second argument, which is optional, is a string that should be inserted as a header comment if fixing is enabled and no header that matches the first argument is found. The third argument, which is optional, is a string that denotes whether or not a newline should exist on the header.`, options: { - type: "array", - items: [ + oneOf: [ { - type: "string", + type: "array", + items: { + type: "object", + properties: { + [OPTION_MATCH]: { + type: "string", + }, + [OPTION_ALLOW_SINGLE_LINE_COMMENTS]: { + type: "boolean", + }, + [OPTION_DEFAULT]: { + type: "string", + }, + [OPTION_ENFORCE_TRAILING_NEWLINE]: { + type: "boolean", + }, + }, + additionalProperties: false, + }, }, { - type: "string", + type: "array", + items: [ + { + type: "string", + }, + { + type: "string", + }, + { + type: "string", + }, + ], + additionalItems: false, + minLength: 1, + maxLength: 3, }, + ], + }, + optionExamples: [ + [ + true, { - type: "string", + [OPTION_MATCH]: "Copyright \\d{4}", + [OPTION_ALLOW_SINGLE_LINE_COMMENTS]: true, + [OPTION_DEFAULT]: "Copyright 2018", + [OPTION_ENFORCE_TRAILING_NEWLINE]: true, }, ], - additionalItems: false, - minLength: 1, - maxLength: 3, - }, - optionExamples: [[true, "Copyright \\d{4}", "Copyright 2018", ENFORCE_TRAILING_NEWLINE]], + [true, "Copyright \\d{4}", "Copyright 2018", OPTION_ENFORCE_TRAILING_NEWLINE], + ], hasFix: true, type: "style", typescriptOnly: false, @@ -61,17 +118,18 @@ export class Rule extends Lint.Rules.AbstractRule { public static MISSING_NEW_LINE_FAILURE_STRING = "missing new line following the file header"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + const options = this.getRuleOptions(); + const { text } = sourceFile; - const headerFormat = new RegExp(this.ruleArguments[0] as string); - const textToInsert = this.ruleArguments[1] as string | undefined; - const enforceExtraTrailingLine = - this.ruleArguments.indexOf(ENFORCE_TRAILING_NEWLINE) !== -1; + const headerFormat = new RegExp(options[OPTION_MATCH]); + const textToInsert = options[OPTION_DEFAULT]; // ignore shebang if it exists let offset = text.startsWith("#!") ? text.indexOf("\n") : 0; - // returns the text of the first comment or undefined - const commentText = ts.forEachLeadingCommentRange(text, offset, (pos, end, kind) => - text.substring(pos + 2, kind === ts.SyntaxKind.SingleLineCommentTrivia ? end : end - 2), + const commentText = this.getFileHeaderText( + text, + offset, + !!options[OPTION_ALLOW_SINGLE_LINE_COMMENTS], ); if (commentText === undefined || !headerFormat.test(commentText)) { @@ -107,7 +165,7 @@ export class Rule extends Lint.Rules.AbstractRule { } const trailingNewLineViolation = - enforceExtraTrailingLine && + options[OPTION_ENFORCE_TRAILING_NEWLINE] && headerFormat.test(commentText) && this.doesNewLineEndingViolationExist(text, offset); @@ -135,6 +193,24 @@ export class Rule extends Lint.Rules.AbstractRule { return []; } + private getRuleOptions(): FileHeaderRuleOptions { + const options = this.ruleArguments; + if (options.length === 1 && typeof options[0] === "object") { + return options[0] as FileHeaderRuleOptions; + } + + // Legacy options + const args = this.ruleArguments as string[]; + return { + [OPTION_DEFAULT]: args[1], + [OPTION_ENFORCE_TRAILING_NEWLINE]: + args[2] !== undefined + ? args[2].indexOf(OPTION_ENFORCE_TRAILING_NEWLINE) !== -1 + : undefined, + [OPTION_MATCH]: args[0], + }; + } + private createComment( sourceFile: ts.SourceFile, commentText: string, @@ -172,4 +248,26 @@ export class Rule extends Lint.Rules.AbstractRule { entireComment !== undefined && NEW_LINE_FOLLOWING_HEADER.test(entireComment) !== null ); } + + private getFileHeaderText( + text: string, + offset: number, + allowSingleLineComments: boolean, + ): string | undefined { + const ranges = ts.getLeadingCommentRanges(text, offset); + if (ranges === undefined || ranges.length === 0) { + return undefined; + } + + const fileHeaderRanges = !allowSingleLineComments ? ranges.slice(0, 1) : ranges; + return fileHeaderRanges + .map(range => { + const { pos, kind, end } = range; + return text.substring( + pos + 2, + kind === ts.SyntaxKind.SingleLineCommentTrivia ? end : end - 2, + ); + }) + .join("\n"); + } } diff --git a/test/rules/file-header/bad/test2.ts.fix b/test/rules/file-header/bad/test2.ts.fix new file mode 100644 index 00000000000..29f5b546f57 --- /dev/null +++ b/test/rules/file-header/bad/test2.ts.fix @@ -0,0 +1,7 @@ +/*! + * Good header 2 + */ + +// ********************************** +// Bad header +// ********************************** diff --git a/test/rules/file-header/bad/test2.ts.lint b/test/rules/file-header/bad/test2.ts.lint new file mode 100644 index 00000000000..ef259de4caf --- /dev/null +++ b/test/rules/file-header/bad/test2.ts.lint @@ -0,0 +1,4 @@ +// ********************************** +~nil [missing file header] +// Bad header +// ********************************** diff --git a/test/rules/file-header/good-allow-single-line-comments/test.ts.lint b/test/rules/file-header/good-allow-single-line-comments/test.ts.lint new file mode 100644 index 00000000000..8aa13d57e2e --- /dev/null +++ b/test/rules/file-header/good-allow-single-line-comments/test.ts.lint @@ -0,0 +1,4 @@ +// ********************************** +// Good header +// ********************************** + diff --git a/test/rules/file-header/good-allow-single-line-comments/tslint.json b/test/rules/file-header/good-allow-single-line-comments/tslint.json new file mode 100644 index 00000000000..5f1256bb402 --- /dev/null +++ b/test/rules/file-header/good-allow-single-line-comments/tslint.json @@ -0,0 +1,8 @@ +{ + "rules": { + "file-header": [true, { + "match": "Good header", + "allow-single-line-comments": true + }] + } + }