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

Address 3180: Rule file-header should have an option to allow // comments #4560

Merged
merged 4 commits into from
Mar 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 117 additions & 19 deletions src/rules/fileHeaderRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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: {
rrogowski marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -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)) {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
}
7 changes: 7 additions & 0 deletions test/rules/file-header/bad/test2.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*!
* Good header 2
*/

// **********************************
// Bad header
// **********************************
4 changes: 4 additions & 0 deletions test/rules/file-header/bad/test2.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// **********************************
~nil [missing file header]
// Bad header
// **********************************
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// **********************************
// Good header
// **********************************

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"rules": {
"file-header": [true, {
"match": "Good header",
"allow-single-line-comments": true
}]
}
}