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);
+ });
+});