diff --git a/README.md b/README.md index 1e06920d998..48744a5d9c3 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,14 @@ Supported Rules ----- * `class-name` enforces PascalCased class and interface names. +* `comment-format` enforces rules for single-line comments. Rule options: + * `"check-space"` enforces the rule that all single-line comments must begin with a space, as in `// comment` + * `"check-lowercase"` enforces the rule that the first non-whitespace character of a comment must be lowercase, if applicable * `curly` enforces braces for `if`/`for`/`do`/`while` statements. * `eofline` enforces the file to end with a newline. * `forin` enforces a `for ... in` statement to be filtered with an `if` statement.* * `indent` enforces consistent indentation levels (currently disabled). -* `interface-name` enforces the rule that interface names must begin with a capital 'I' +* `interface-name` enforces the rule that interface names must begin with a capital 'I' * `label-position` enforces labels only on sensible statements. * `label-undefined` checks that labels are defined before usage. * `max-line-length` sets the maximum length of a line. diff --git a/src/rules/commentFormatRule.ts b/src/rules/commentFormatRule.ts new file mode 100644 index 00000000000..ff9228ce379 --- /dev/null +++ b/src/rules/commentFormatRule.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2013 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/// + +var OPTION_SPACE = "check-space"; +var OPTION_LOWERCASE = "check-lowercase"; + +export class Rule extends Lint.Rules.AbstractRule { + static LOWERCASE_FAILURE = "comment must start with lowercase letter"; + static LEADING_SPACE_FAILURE = "comment must start with a space"; + + public apply(syntaxTree: TypeScript.SyntaxTree): Lint.RuleFailure[] { + return this.applyWithWalker(new CommentWalker(syntaxTree, this.getOptions())); + } +} + +class CommentWalker extends Lint.RuleWalker { + public visitToken(token: TypeScript.ISyntaxToken): void { + this.findFailuresForTrivia(token.leadingTrivia().toArray(), this.position()); + this.findFailuresForTrivia(token.trailingTrivia().toArray(), this.position() + token.leadingTriviaWidth() + token.width()); + + super.visitToken(token); + } + + private findFailuresForTrivia(triviaList: TypeScript.ISyntaxTrivia[], startingPosition: number) { + var currentPosition = startingPosition; + triviaList.forEach((triviaItem) => { + if (triviaItem.kind() === TypeScript.SyntaxKind.SingleLineCommentTrivia) { + var commentText = triviaItem.fullText(); + if (this.hasOption(OPTION_SPACE)) { + if (!this.startsWithSpace(commentText)) { + var leadingSpaceFailure = this.createFailure(currentPosition, triviaItem.fullWidth(), Rule.LEADING_SPACE_FAILURE); + this.addFailure(leadingSpaceFailure); + } + } + if (this.hasOption(OPTION_LOWERCASE)) { + if (!this.startsWithLowercase(commentText)) { + var lowercaseFailure = this.createFailure(currentPosition, triviaItem.fullWidth(), Rule.LOWERCASE_FAILURE); + this.addFailure(lowercaseFailure); + } + } + } + currentPosition += triviaItem.fullWidth(); + }); + } + + private startsWithSpace(commentText: string): boolean { + if (commentText.length <= 2) { + return true; // comment is "//"? Technically not a violation. + } + + var firstCharacter = commentText.charAt(2); // first character after the space + return firstCharacter === " "; + } + + private startsWithLowercase(commentText: string): boolean { + if (commentText.length <= 2) { + return true; // comment is "//"? Technically not a violation. + } + + // regex is "start of string"//"any amount of whitespace"("word character") + var firstCharacterMatch = commentText.match(/^\/\/\s*(\w)/); + if (firstCharacterMatch != null) { + // the first group matched, i.e. the thing in the parens, is the first non-space character, if it's alphanumeric + var firstCharacter = firstCharacterMatch[1]; + return firstCharacter === firstCharacter.toLowerCase(); + } else { + // first character isn't alphanumeric/doesn't exist? Technically not a violation + return true; + } + } + +} diff --git a/test/files/rules/comment.test.ts b/test/files/rules/comment.test.ts new file mode 100644 index 00000000000..8e27409fd9d --- /dev/null +++ b/test/files/rules/comment.test.ts @@ -0,0 +1,9 @@ +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 + console.log("test"); //this comment has no space + } +} diff --git a/test/rules/commentFormatRuleTests.ts b/test/rules/commentFormatRuleTests.ts new file mode 100644 index 00000000000..92cd06859fb --- /dev/null +++ b/test/rules/commentFormatRuleTests.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2013 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +/// + +describe("", () => { + var CommentFormatRule = Lint.Test.getRule("comment-format"); + + it("ensures comments start with a space and a lowercase letter", () => { + var fileName = "rules/comment.test.ts"; + var createLowercaseFailure = Lint.Test.createFailuresOnFile(fileName, CommentFormatRule.LOWERCASE_FAILURE); + var createLeadingSpaceFailure = Lint.Test.createFailuresOnFile(fileName, CommentFormatRule.LEADING_SPACE_FAILURE); + var expectedFailure1 = createLowercaseFailure([5, 25], [5, 73]); + var expectedFailure2 = createLowercaseFailure([6, 9], [6, 84]); + var expectedFailure3 = createLeadingSpaceFailure([6, 9], [6, 84]); + var expectedFailure4 = createLeadingSpaceFailure([7, 30], [7, 57]); + + var options = [true, + "check-space", + "check-lowercase" + ]; + var actualFailures = Lint.Test.applyRuleOnFile(fileName, CommentFormatRule, options); + + Lint.Test.assertContainsFailure(actualFailures, expectedFailure1); + Lint.Test.assertContainsFailure(actualFailures, expectedFailure2); + Lint.Test.assertContainsFailure(actualFailures, expectedFailure3); + Lint.Test.assertContainsFailure(actualFailures, expectedFailure4); + }); +});