diff --git a/src/rules/fileHeaderRule.ts b/src/rules/fileHeaderRule.ts index 8d943b6760a..44eb1c3a5f4 100644 --- a/src/rules/fileHeaderRule.ts +++ b/src/rules/fileHeaderRule.ts @@ -23,11 +23,26 @@ export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "file-header", description: "Enforces a certain header comment for all files, matched by a regular expression.", - optionsDescription: "Regular expression to match the header.", + optionsDescription: Lint.Utils.dedent` + 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.`, options: { - type: "string", + type: "array", + items: [ + { + type: "string", + }, + { + type: "string", + }, + ], + additionalItems: false, + minLength: 1, + maxLength: 2, }, - optionExamples: [[true, "Copyright \\d{4}"]], + optionExamples: [[true, "Copyright \\d{4}", "Copyright 2017"]], + hasFix: true, type: "style", typescriptOnly: false, }; @@ -37,6 +52,9 @@ export class Rule extends Lint.Rules.AbstractRule { public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { const { text } = sourceFile; + const headerFormat = new RegExp(this.ruleArguments[0] as string); + const textToInsert = this.ruleArguments[1] as string | undefined; + // ignore shebang if it exists let offset = text.startsWith("#!") ? text.indexOf("\n") : 0; // returns the text of the first comment or undefined @@ -44,12 +62,32 @@ export class Rule extends Lint.Rules.AbstractRule { text, offset, (pos, end, kind) => text.substring(pos + 2, kind === ts.SyntaxKind.SingleLineCommentTrivia ? end : end - 2)); - if (commentText === undefined || !new RegExp(this.ruleArguments[0] as string).test(commentText)) { - if (offset !== 0) { + + if (commentText === undefined || !headerFormat.test(commentText)) { + const isErrorAtStart = offset === 0; + if (!isErrorAtStart) { ++offset; // show warning in next line after shebang } - return [new Lint.RuleFailure(sourceFile, offset, offset, Rule.FAILURE_STRING, this.ruleName)]; + const leadingNewlines = isErrorAtStart ? 0 : 1; + const trailingNewlines = isErrorAtStart ? 2 : 1; + + const fix = textToInsert !== undefined + ? Lint.Replacement.appendText(offset, this.createComment(sourceFile, textToInsert, leadingNewlines, trailingNewlines)) + : undefined; + return [new Lint.RuleFailure(sourceFile, offset, offset, Rule.FAILURE_STRING, this.ruleName, fix)]; } return []; } + + private createComment(sourceFile: ts.SourceFile, commentText: string, leadingNewlines = 1, trailingNewlines = 1) { + const maybeCarriageReturn = sourceFile.text[sourceFile.getLineEndOfPosition(0)] === "\r" ? "\r" : ""; + const lineEnding = `${maybeCarriageReturn}\n`; + return lineEnding.repeat(leadingNewlines) + [ + "/*", + // split on both types of line endings in case users just typed "\n" in their configs + // but are working in files with \r\n line endings + ...commentText.split(/\r?\n/g).map((line) => ` * ${line}`), + " */", + ].join(lineEnding) + lineEnding.repeat(trailingNewlines); + } } diff --git a/test/rules/file-header/bad-shebang/test.ts.fix b/test/rules/file-header/bad-shebang/test.ts.fix new file mode 100644 index 00000000000..25052a6fee4 --- /dev/null +++ b/test/rules/file-header/bad-shebang/test.ts.fix @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/* + * Good header 2 + */ + +/* + * Bad header 3 + */ + +export class A { + public x = 1; + + public B() { + return 2; + } +} + +/* + * Good header 4 + */ diff --git a/test/rules/file-header/bad-shebang/tslint.json b/test/rules/file-header/bad-shebang/tslint.json index 79f26da8b41..9203b6ffb9a 100644 --- a/test/rules/file-header/bad-shebang/tslint.json +++ b/test/rules/file-header/bad-shebang/tslint.json @@ -1,5 +1,5 @@ { "rules": { - "file-header": [true, "Good header \\d"] + "file-header": [true, "Good header \\d", "Good header 2"] } } diff --git a/test/rules/file-header/bad-single-line/test.ts.fix b/test/rules/file-header/bad-single-line/test.ts.fix new file mode 100644 index 00000000000..69c8e361f7c --- /dev/null +++ b/test/rules/file-header/bad-single-line/test.ts.fix @@ -0,0 +1,17 @@ +/* + * Good header 2 + */ + +// Bad header 1 + +export class A { + public x = 1; + + public B() { + return 2; + } +} + +/* + * Good header 2 + */ diff --git a/test/rules/file-header/bad-single-line/tslint.json b/test/rules/file-header/bad-single-line/tslint.json index 79f26da8b41..9203b6ffb9a 100644 --- a/test/rules/file-header/bad-single-line/tslint.json +++ b/test/rules/file-header/bad-single-line/tslint.json @@ -1,5 +1,5 @@ { "rules": { - "file-header": [true, "Good header \\d"] + "file-header": [true, "Good header \\d", "Good header 2"] } } diff --git a/test/rules/file-header/bad-use-strict/test.ts.fix b/test/rules/file-header/bad-use-strict/test.ts.fix new file mode 100644 index 00000000000..6104810090f --- /dev/null +++ b/test/rules/file-header/bad-use-strict/test.ts.fix @@ -0,0 +1,21 @@ +/* + * Good header 2 + */ + +"use strict"; + +/* + * Bad header 5 + */ + +export class A { + public x = 1; + + public B() { + return 2; + } +} + +/* + * Good header 6 + */ diff --git a/test/rules/file-header/bad-use-strict/tslint.json b/test/rules/file-header/bad-use-strict/tslint.json index 79f26da8b41..9203b6ffb9a 100644 --- a/test/rules/file-header/bad-use-strict/tslint.json +++ b/test/rules/file-header/bad-use-strict/tslint.json @@ -1,5 +1,5 @@ { "rules": { - "file-header": [true, "Good header \\d"] + "file-header": [true, "Good header \\d", "Good header 2"] } } diff --git a/test/rules/file-header/bad/test.ts.fix b/test/rules/file-header/bad/test.ts.fix new file mode 100644 index 00000000000..cebf0451d0b --- /dev/null +++ b/test/rules/file-header/bad/test.ts.fix @@ -0,0 +1,19 @@ +/* + * Good header 2 + */ + +/* + * Bad header 1 + */ + +export class A { + public x = 1; + + public B() { + return 2; + } +} + +/* + * Good header 2 + */ diff --git a/test/rules/file-header/bad/tslint.json b/test/rules/file-header/bad/tslint.json index 79f26da8b41..9203b6ffb9a 100644 --- a/test/rules/file-header/bad/tslint.json +++ b/test/rules/file-header/bad/tslint.json @@ -1,5 +1,5 @@ { "rules": { - "file-header": [true, "Good header \\d"] + "file-header": [true, "Good header \\d", "Good header 2"] } } diff --git a/test/rules/file-header/good/test.ts.fix b/test/rules/file-header/good/test.ts.fix new file mode 100644 index 00000000000..df6b2bb9a56 --- /dev/null +++ b/test/rules/file-header/good/test.ts.fix @@ -0,0 +1,12 @@ + +/* + * Good header 1 + */ + +export class A { + public x = 1; + + public B() { + return 2; + } +}