--- /dev/null
+/**
+ * @fileoverview Rule to enforce var declarations are only at the top of a function.
+ * @author Danny Fritz
+ * @author Gyandeep Singh
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "suggestion",
+
+ docs: {
+ description: "require `var` declarations be placed at the top of their containing scope",
+ category: "Best Practices",
+ recommended: false,
+ url: "https://eslint.org/docs/rules/vars-on-top"
+ },
+
+ schema: [],
+ messages: {
+ top: "All 'var' declarations must be at the top of the function scope."
+ }
+ },
+
+ create(context) {
+
+ //--------------------------------------------------------------------------
+ // Helpers
+ //--------------------------------------------------------------------------
+
+ // eslint-disable-next-line jsdoc/require-description
+ /**
+ * @param {ASTNode} node any node
+ * @returns {boolean} whether the given node structurally represents a directive
+ */
+ function looksLikeDirective(node) {
+ return node.type === "ExpressionStatement" &&
+ node.expression.type === "Literal" && typeof node.expression.value === "string";
+ }
+
+ /**
+ * Check to see if its a ES6 import declaration
+ * @param {ASTNode} node any node
+ * @returns {boolean} whether the given node represents a import declaration
+ */
+ function looksLikeImport(node) {
+ return node.type === "ImportDeclaration" || node.type === "ImportSpecifier" ||
+ node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier";
+ }
+
+ /**
+ * Checks whether a given node is a variable declaration or not.
+ * @param {ASTNode} node any node
+ * @returns {boolean} `true` if the node is a variable declaration.
+ */
+ function isVariableDeclaration(node) {
+ return (
+ node.type === "VariableDeclaration" ||
+ (
+ node.type === "ExportNamedDeclaration" &&
+ node.declaration &&
+ node.declaration.type === "VariableDeclaration"
+ )
+ );
+ }
+
+ /**
+ * Checks whether this variable is on top of the block body
+ * @param {ASTNode} node The node to check
+ * @param {ASTNode[]} statements collection of ASTNodes for the parent node block
+ * @returns {boolean} True if var is on top otherwise false
+ */
+ function isVarOnTop(node, statements) {
+ const l = statements.length;
+ let i = 0;
+
+ // skip over directives
+ for (; i < l; ++i) {
+ if (!looksLikeDirective(statements[i]) && !looksLikeImport(statements[i])) {
+ break;
+ }
+ }
+
+ for (; i < l; ++i) {
+ if (!isVariableDeclaration(statements[i])) {
+ return false;
+ }
+ if (statements[i] === node) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether variable is on top at the global level
+ * @param {ASTNode} node The node to check
+ * @param {ASTNode} parent Parent of the node
+ * @returns {void}
+ */
+ function globalVarCheck(node, parent) {
+ if (!isVarOnTop(node, parent.body)) {
+ context.report({ node, messageId: "top" });
+ }
+ }
+
+ /**
+ * Checks whether variable is on top at functional block scope level
+ * @param {ASTNode} node The node to check
+ * @param {ASTNode} parent Parent of the node
+ * @param {ASTNode} grandParent Parent of the node's parent
+ * @returns {void}
+ */
+ function blockScopeVarCheck(node, parent, grandParent) {
+ if (!(/Function/u.test(grandParent.type) &&
+ parent.type === "BlockStatement" &&
+ isVarOnTop(node, parent.body))) {
+ context.report({ node, messageId: "top" });
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // Public API
+ //--------------------------------------------------------------------------
+
+ return {
+ "VariableDeclaration[kind='var']"(node) {
+ if (node.parent.type === "ExportNamedDeclaration") {
+ globalVarCheck(node.parent, node.parent.parent);
+ } else if (node.parent.type === "Program") {
+ globalVarCheck(node, node.parent);
+ } else {
+ blockScopeVarCheck(node, node.parent, node.parent.parent);
+ }
+ }
+ };
+
+ }
+};