Skip to content

Commit

Permalink
New: lines-around-directive rule (fixes #6069)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaicataldo committed Aug 23, 2016
1 parent a5189a6 commit c770f91
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
185 changes: 185 additions & 0 deletions lib/rules/lines-around-directive.js
@@ -0,0 +1,185 @@
/**
* @fileoverview Enforce or disallow newlines around directives
* @author Kai Cataldo
*/

"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: "enforce or disallow newlines around directives",
category: "Stylistic Issues",
recommended: false
},
schema: [{
oneOf: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
before: {
enum: ["always", "never"]
},
after: {
enum: ["always", "never"]
},
},
additionalProperties: false,
minProperties: 2
}
]
}]
},

create(context) {
const sourceCode = context.getSourceCode();
const config = context.options[0] || "always";
const mode = generateMode(config);

//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------

/**
* Generate rule mode from rule options configuration.
* @param {object|string} config Configuration from rule options.
* @returns {object} Represents whether or not rule should check "before" and "after".
*/
function generateMode(config) {
if (typeof config === "string") {
return {
before: config,
after: config
};
} else if (typeof config === "object") {
return {
before: config.before,
after: config.after
};
}
}

/**
* Check if node is a 'use strict' directive.
* @param {ASTNode} node Node to check.
* @returns {boolean} Whether or not the passed in node is a 'use strict' directive.
*/
function isNodeUseStrictDirective(node) {
return node.type === "ExpressionStatement" &&
node.expression.type === "Literal" &&
node.expression.value === "use strict";
}

/**
* Get 'use strict' directive from node body.
* @param {ASTNode[]} body Body of node to check.
* @returns {boolean} Whether or not the passed in node is preceded by a blank newline.
*/
function getUseStrictDirective(body) {
const firstStatement = body[0];
return isNodeUseStrictDirective(firstStatement) ? firstStatement : null;
}

/**
* Check if node is preceded by a blank newline.
* @param {ASTNode} node Node to check.
* @returns {boolean} Whether or not the passed in node is preceded by a blank newline.
*/
function hasNewlineBefore(node, comments) {
const comment = comments[comments.length - 1];
return node.loc.start.line - comment.loc.end.line >= 2;
}

/**
* Check if node is followed by a blank newline.
* @param {ASTNode} node Node to check.
* @returns {boolean} Whether or not the passed in node is followed by a blank newline.
*/
function hasNewlineAfter(node) {
const trailingComments = sourceCode.getComments(node).trailing;
const tokenAfter = trailingComments.length ? trailingComments[0] : sourceCode.getTokenAfter(node);
return tokenAfter.loc.start.line - node.loc.end.line >= 2;
}

/**
* Report errors for newlines around directives.
* @param {ASTNode} node Node to check.
* @param {string} location Whether the error was found before or after the directive.
* @param {boolean} expected Whether or not a newline was expected or unexpected.
* @returns {void}
*/
function reportError(node, location, expected) {
context.report({
node,
message: "{{expected}} newline {{location}} 'use strict' directive.",
data: {
expected: expected ? "Expected" : "Unexpected",
location
}
});
}

/**
* Check lines around directives in node
* @param {ASTNode} node - node to check
* @returns {void}
*/
function checkDirectives(node) {
// Skip arrow functions with implicit return.
// `() => "use strict";` returns the string "use strict".
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
return;
}

const body = node.type === "Program" ? node.body : node.body.body;
const directive = getUseStrictDirective(body);

if (!directive) {
return;
}

// Only check before if directive has leading comments
const leadingComments = directive.leadingComments;
if (leadingComments && leadingComments.length) {
if (mode.before === "always" && !hasNewlineBefore(directive, leadingComments)) {
reportError(directive, "before", true);
}

if (mode.before === "never" && hasNewlineBefore(directive, leadingComments)) {
reportError(directive, "before", false);
}
}

// Do not check after if the directive is the last statement in the body of a Program or Function
if (directive === body[body.length - 1]) {
return;
}

if (mode.after === "always" && !hasNewlineAfter(directive)) {
reportError(directive, "after", true);
}

if (mode.after === "never" && hasNewlineAfter(directive)) {
reportError(directive, "after", false);
}
}

//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------

return {
Program: checkDirectives,
FunctionDeclaration: checkDirectives,
FunctionExpression: checkDirectives,
ArrowFunctionExpression: checkDirectives
};
}
}
153 changes: 153 additions & 0 deletions tests/lib/rules/lines-around-directive.js
@@ -0,0 +1,153 @@
/**
* @fileoverview Enforce newlines between operands of ternary expressions
* @author Kai Cataldo
*/

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/lines-around-directive");
const RuleTester = require("../../../lib/testers/rule-tester");

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();

ruleTester.run("lines-around-directive", rule, {
valid: [

// Uses "always by default
"// comment\n\n'use strict';\n\nvar foo;",

// "always"
// at top of file
{ code: "'use strict';\n\nvar foo;", options: ["always"] },
{ code: "'use strict';\n\n//comment", options: ["always"] },
{ code: "'use strict';\n\n/*comment*/", options: ["always"] },

// after comment at top of file
{ code: "#!/usr/bin/env node\n\n'use strict';\n\nvar foo;", options: ["always"] },
{ code: "// comment\n\n'use strict';\n\nvar foo;", options: ["always"] },
{ code: "/*comment*/\n\n'use strict';\n\nvar foo;", options: ["always"] },
{ code: "/*\nmultiline comment\n*/\n\n'use strict';\n\nvar foo;", options: ["always"] },

// at the top of function blocks
{ code: "function foo() {\n'use strict';\n\nvar bar;\n}", options: ["always"] },
{ code: "function foo() {\n\n'use strict';\n\nvar bar;\n}", options: ["always"] },
{ code: "() => {\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["always"] },
{ code: "() => {\n\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["always"] },

// is not affected by JSDoc comments when at top of function block
{ code: "/*\n * JSDoc comment\n */\nfunction foo() {\n'use strict';\n\nvar bar;\n}", options: ["always"] },

// ignores if the directive is the only statement in the function block
{ code: "function foo() {\n'use strict';\n}", options: ["always"] },

// after comment at top of function blocks
{ code: "function foo() {\n// comment\n\n'use strict';\n\nvar bar;\n}", options: ["always"] },
{ code: "function foo() {\n/*\nmultiline comment\n*/\n\n'use strict';\n\nvar bar;\n}", options: ["always"] },
{ code: "() => {\n// comment\n\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["always"] },
{ code: "() => {\n/*\nmultiline comment\n*/\n\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["always"] },


// "never"
// at top of file
{ code: "'use strict';\nvar foo;", options: ["never"] },
{ code: "'use strict';\n//comment", options: ["never"] },
{ code: "'use strict';\n/*comment*/", options: ["never"] },

// after comment at top of file
{ code: "#!/usr/bin/env node\n'use strict';\nvar foo;", options: ["never"] },
{ code: "// comment\n'use strict';\nvar foo;", options: ["never"] },
{ code: "/*comment*/\n'use strict';\nvar foo;", options: ["never"] },
{ code: "/*\nmultiline comment\n*/\n'use strict';\nvar foo;", options: ["never"] },

// at the top of function blocks
{ code: "function foo() {\n'use strict';\nvar bar;\n}", options: ["never"] },
{ code: "function foo() {\n\n'use strict';\nvar bar;\n}", options: ["never"] },
{ code: "() => {\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["never"] },
{ code: "() => {\n\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["never"] },

// is not affected by JSDoc comments when at top of function block
{ code: "/*\n * JSDoc comment\n */\nfunction foo() {\n'use strict';\nvar bar;\n}", options: ["never"] },

// does not throw if the directive is the only statement in the function block
{ code: "function foo() {\n'use strict';\n}", options: ["never"] },

// after comment at top of function blocks
{ code: "function foo() {\n// comment\n'use strict';\nvar bar;\n}", options: ["never"] },
{ code: "function foo() {\n/*\nmultiline comment\n*/\n'use strict';\nvar bar;\n}", options: ["never"] },
{ code: "() => {\n// comment\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["never"] },
{ code: "() => {\n/*\nmultiline comment\n*/\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: ["never"] },


// { "after": "always", "before": "never" }
// at top of file
{ code: "'use strict';\n\nvar foo;", options: [{ after: "always", before: "never" }] },
{ code: "'use strict';\n\n//comment", options: [{ after: "always", before: "never" }] },
{ code: "'use strict';\n\n/*comment*/", options: [{ after: "always", before: "never" }] },

// after comment at top of file
{ code: "#!/usr/bin/env node\n'use strict';\n\nvar foo;", options: [{ after: "always", before: "never" }] },
{ code: "// comment\n'use strict';\n\nvar foo;", options: [{ after: "always", before: "never" }] },
{ code: "/*comment*/\n'use strict';\n\nvar foo;", options: [{ after: "always", before: "never" }] },
{ code: "/*\nmultiline comment\n*/\n'use strict';\n\nvar foo;", options: [{ after: "always", before: "never" }] },

// at the top of function blocks
{ code: "function foo() {\n'use strict';\n\nvar bar;\n}", options: [{ after: "always", before: "never" }] },
{ code: "function foo() {\n\n'use strict';\n\nvar bar;\n}", options: [{ after: "always", before: "never" }] },
{ code: "() => {\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "always", before: "never" }] },
{ code: "() => {\n\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "always", before: "never" }] },

// is not affected by JSDoc comments when at top of function block
{ code: "/*\n * JSDoc comment\n */\nfunction foo() {\n'use strict';\n\nvar bar;\n}", options: [{ after: "always", before: "never" }] },

// does not throw if the directive is the only statement in the function block
{ code: "function foo() {\n'use strict';\n}", options: [{ after: "always", before: "never" }] },

// after comment at top of function blocks
{ code: "function foo() {\n// comment\n'use strict';\n\nvar bar;\n}", options: [{ after: "always", before: "never" }] },
{ code: "function foo() {\n/*\nmultiline comment\n*/\n'use strict';\n\nvar bar;\n}", options: [{ after: "always", before: "never" }] },
{ code: "() => {\n// comment\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "always", before: "never" }] },
{ code: "() => {\n/*\nmultiline comment\n*/\n'use strict';\n\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "always", before: "never" }] },


// { "after": "never", "before": "always" }
// at top of file
{ code: "'use strict';\nvar foo;", options: [{ after: "never", before: "always" }] },
{ code: "'use strict';\n//comment", options: [{ after: "never", before: "always" }] },
{ code: "'use strict';\n/*comment*/", options: [{ after: "never", before: "always" }] },

// after comment at top of file
{ code: "#!/usr/bin/env node\n\n'use strict';\nvar foo;", options: [{ after: "never", before: "always" }] },
{ code: "// comment\n\n'use strict';\nvar foo;", options: [{ after: "never", before: "always" }] },
{ code: "/*comment*/\n\n'use strict';\nvar foo;", options: [{ after: "never", before: "always" }] },
{ code: "/*\nmultiline comment\n*/\n\n'use strict';\nvar foo;", options: [{ after: "never", before: "always" }] },

// at the top of function blocks
{ code: "function foo() {\n'use strict';\nvar bar;\n}", options: [{ after: "never", before: "always" }] },
{ code: "function foo() {\n\n'use strict';\nvar bar;\n}", options: [{ after: "never", before: "always" }] },
{ code: "() => {\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "never", before: "always" }] },
{ code: "() => {\n\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "never", before: "always" }] },

// is not affected by JSDoc comments when at top of function block
{ code: "/*\n * JSDoc comment\n */\nfunction foo() {\n'use strict';\nvar bar;\n}", options: [{ after: "never", before: "always" }] },

// does not throw if the directive is the only statement in the function block
{ code: "function foo() {\n'use strict';\n}", options: [{ after: "never", before: "always" }] },

// after comment at top of function blocks
{ code: "function foo() {\n// comment\n\n'use strict';\nvar bar;\n}", options: [{ after: "never", before: "always" }] },
{ code: "function foo() {\n/*\nmultiline comment\n*/\n\n'use strict';\nvar bar;\n}", options: [{ after: "never", before: "always" }] },
{ code: "() => {\n// comment\n\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "never", before: "always" }] },
{ code: "() => {\n/*\nmultiline comment\n*/\n\n'use strict';\nvar foo;\n}", parserOptions: { ecmaVersion: 6 }, options: [{ after: "never", before: "always" }] },
],

invalid: [
]
});

0 comments on commit c770f91

Please sign in to comment.