Skip to content

Commit

Permalink
WIP New: lines-around-directive rule (fixes #6069)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaicataldo committed Aug 25, 2016
1 parent a5189a6 commit ea43f89
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 0 deletions.
184 changes: 184 additions & 0 deletions lib/rules/lines-around-directive.js
@@ -0,0 +1,184 @@
/**
* @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";
let checkBefore;
let checkAfter;

if (typeof config === "string") {
checkBefore = config;
checkAfter = config;
} else if (typeof config === "object") {
checkBefore = config.before;
checkAfter = config.after;
}

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

/**
* 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) {
const leadingComments = node.leadingComments;
const lastComment = leadingComments[leadingComments.length - 1];

return node.loc.start.line - lastComment.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. This is to not force a newline
// at the top of the file if there are no comments, as well as for compatibility with
// padded-blocks.
const leadingComments = directive.leadingComments;

if (leadingComments && leadingComments.length) {
if (checkBefore === "always" && !hasNewlineBefore(directive)) {
reportError(directive, "before", true);
}

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

// Do not check after if the directive is the last statement in the body
// of a Program or Block to ensure compatibility with padded-blocks.
if (directive === body[body.length - 1]) {
return;
}

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

if (checkAfter === "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 ea43f89

Please sign in to comment.