diff --git a/index.js b/index.js index 00d4d2c..ae2afeb 100644 --- a/index.js +++ b/index.js @@ -6,9 +6,9 @@ module.exports = { 'no-duplicate-requires': require('./no-duplicate-requires').rule, 'no-missing-requires': require('./no-missing-requires').rule, 'no-unused-requires': require('./no-unused-requires').rule, - 'one-provide': require('./one-provide').rule, + 'one-provide-or-module': require('./one-provide-or-module').rule, 'requires-first': require('./requires-first').rule, - 'valid-provide': require('./valid-provide').rule, + 'valid-provide-and-module': require('./valid-provide-and-module').rule, 'valid-requires': require('./valid-requires').rule } }; diff --git a/no-missing-requires.js b/no-missing-requires.js index 6515291..926453f 100644 --- a/no-missing-requires.js +++ b/no-missing-requires.js @@ -14,8 +14,6 @@ const util = require('./util'); * but ol.ext.foo would require ol.ext.foo) */ -const CONST_RE = /^(ol(\.[a-z]\w*)*)\.[A-Z]+_([_A-Z])+$/; -const CLASS_RE = /^(ol(\.[a-z]\w*)*\.[A-Z]\w*)(\.\w+)*$/; exports.rule = { meta: { @@ -27,11 +25,16 @@ exports.rule = { create: function(context) { const defined = {}; + const prefixes = context.options[0].prefixes || ['ol']; + const joined = prefixes.join('|'); + const CONST_RE = new RegExp(`^((?:${joined})(\\.[a-z]\\w*)*)\\.[A-Z]+_([_A-Z])+$`); + const CLASS_RE = new RegExp(`^((?:${joined})(\\.[a-z]\\w*)*\\.[A-Z]\\w*)(\\.\\w+)*$`); + const STARTS_WITH = new RegExp(`^(?:${joined})\\..*`); return { ExpressionStatement: function(statement) { - if (util.isRequireStatement(statement) || util.isProvideStatement(statement)) { + if (util.isRequireStatement(statement) || util.isRequireVariableDeclaration(statement) || util.isProvideStatement(statement)) { const expression = statement.expression; const arg = expression.arguments[0]; if (!arg || !arg.value) { @@ -43,9 +46,17 @@ exports.rule = { MemberExpression: function(expression) { const parent = expression.parent; + if (parent.type === 'AssignmentExpression') { + const name = util.getName(expression); + if (name && STARTS_WITH.test(name)) { + defined[name] = true; + return; + } + } + if (parent.type !== 'MemberExpression') { const name = util.getName(expression); - if (name && name.startsWith('ol.')) { + if (name && STARTS_WITH.test(name) && !defined[name]) { // check if the name looks like a const let match = name.match(CONST_RE); if (match) { @@ -70,8 +81,9 @@ exports.rule = { } return; } - if (!defined[className]) { - context.report(expression, `Missing goog.require('${className}')`); + const parentObjectName = className.split('.').slice(0, -1).join('.'); // app.constants.RouteType enum in constants namespace + if (!defined[className] && !defined[parentObjectName]) { + context.report(expression, `Missing goog.require('${className}') or goog.require('${parentObjectName}')`); } return; } @@ -82,13 +94,13 @@ exports.rule = { parts.pop(); } const objectName = parts.join('.'); - if (!defined[objectName]) { - context.report(expression, `Missing goog.require('${objectName}')`); + const parentObjectName = parts.slice(0, -1).join('.'); // app.widgets.sortable.dragOptionsDirective.module + if (!defined[objectName] && !defined[parentObjectName]) { + context.report(expression, `Missing goog.require('${objectName}') or goog.require('${parentObjectName}')`); } } } } - }; } }; diff --git a/one-provide-or-module.js b/one-provide-or-module.js new file mode 100644 index 0000000..b2bfb75 --- /dev/null +++ b/one-provide-or-module.js @@ -0,0 +1,43 @@ +'use strict'; + +const util = require('./util'); + +exports.rule = { + meta: { + docs: { + description: 'disallow multiple goog.provide() or goog.module() calls' + } + }, + + create: function(context) { + let hasProvide = false; + let hasModule = false; + + return { + ExpressionStatement: function(statement) { + if (util.isModuleStatement(statement)) { + if (hasModule) { + const name = statement.expression.arguments[0].value; + context.report(statement, `Extra goog.module('${name}')`); + } else if (hasProvide) { + const name = statement.expression.arguments[0].value; + context.report(statement, `Forbidden goog.module('${name}'), a goog.provide() is already present`); + } else { + hasModule = true; + } + } + if (util.isProvideStatement(statement)) { + if (hasModule) { + const name = statement.expression.arguments[0].value; + context.report(statement, `Forbidden goog.provide('${name}'), a goog.module() is already present`); + } else if (hasProvide) { + const name = statement.expression.arguments[0].value; + context.report(statement, `Extra goog.provide('${name}')`); + } else { + hasProvide = true; + } + } + } + }; + } +}; diff --git a/one-provide.js b/one-provide.js deleted file mode 100644 index 092a88e..0000000 --- a/one-provide.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const util = require('./util'); - -exports.rule = { - meta: { - docs: { - description: 'disallow multiple goog.provide() calls' - } - }, - - create: function(context) { - let hasProvide = false; - - return { - ExpressionStatement: function(statement) { - if (util.isProvideStatement(statement)) { - if (hasProvide) { - const name = statement.expression.arguments[0].value; - context.report(statement, `Extra goog.provide('${name}')`); - } else { - hasProvide = true; - } - } - } - }; - } -}; diff --git a/package.json b/package.json index 0c49f6b..b39bfbf 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,22 @@ { - "name": "eslint-plugin-openlayers-internal", + "name": "eslint-plugin-googshift", "version": "3.1.0", "description": "Custom ESLint rules for the OpenLayers project", "main": "index.js", "repository": { "type": "git", - "url": "git://github.com/openlayers/eslint-plugin-openlayers-internal.git" + "url": "git://github.com/gberaudo/eslint-plugin-googshift.git" }, "license": "BSD-2-Clause", "scripts": { "test": "eslint ." }, "eslintConfig": { - "extends": "openlayers", "env": { "es6": true } }, "devDependencies": { - "eslint": "^3.12.2", - "eslint-config-openlayers": "^6.0.0" + "eslint": "^3.12.2" } } diff --git a/requires-first.js b/requires-first.js index 1031a70..bfc3a66 100644 --- a/requires-first.js +++ b/requires-first.js @@ -5,7 +5,7 @@ const util = require('./util'); exports.rule = { meta: { docs: { - description: 'require that all goog.require() precede other statements (except goog.provide())' + description: 'require that all goog.require() precede other statements (except goog.provide(), goog.module() and goog.module.declareLegacyNamespace())' } }, @@ -15,11 +15,11 @@ exports.rule = { let otherSeen = false; program.body.forEach(statement => { - if (util.isRequireStatement(statement)) { + if (util.isRequireStatement(statement) || util.isRequireVariableDeclaration(statement)) { if (otherSeen) { return context.report(statement, 'Expected goog.require() to precede other statements'); } - } else if (!util.isProvideStatement(statement)) { + } else if (!util.isProvideStatement(statement) && !util.isModuleStatement(statement) && !util.isModuleLegacyStatement(statement)) { otherSeen = true; } }); diff --git a/util.js b/util.js index a7c163b..79151b0 100644 --- a/util.js +++ b/util.js @@ -13,6 +13,31 @@ function isGoogStatement(node, name) { isGoogCallExpression(node.expression, name); } +function isGoogVariableDeclaration(node, name) { + return node.type === 'VariableDeclaration' && node.declarations && + node.declarations.length > 0 && isGoogCallExpression(node.declarations[0].init, name); +} + +exports.isModuleExpression = function(node) { + return isGoogCallExpression(node, 'module'); +}; + +exports.isModuleStatement = function(node) { + return isGoogStatement(node, 'module'); +}; + +exports.isModuleLegacyStatement = function(node) { + if (!node.expression || !node.expression.callee || !node.expression.callee.object) { + return false; + } + const callee = node.expression.callee; + return node.expression.type === 'CallExpression' && + callee.object && callee.object.object && + callee.object.object && callee.object.object.name === 'goog' && + callee.object.property && callee.object.property.name === 'module' && + callee.property && callee.property.name === 'declareLegacyNamespace'; +} + exports.isProvideExpression = function(node) { return isGoogCallExpression(node, 'provide'); }; @@ -29,6 +54,10 @@ exports.isRequireStatement = function(node) { return isGoogStatement(node, 'require'); }; +exports.isRequireVariableDeclaration = function(node) { + return isGoogVariableDeclaration(node, 'require'); +}; + var getName = exports.getName = function(node) { if (node.type !== 'MemberExpression') { return; diff --git a/valid-provide.js b/valid-provide-and-module.js similarity index 51% rename from valid-provide.js rename to valid-provide-and-module.js index 9a921a4..e894425 100644 --- a/valid-provide.js +++ b/valid-provide-and-module.js @@ -6,49 +6,55 @@ const util = require('./util'); exports.rule = { meta: { docs: { - description: 'require the first goog.provide() has an arg named like the file path' + description: 'require the first goog.provide() or goog.module() has an arg named like the file path' } }, create: function(context) { + const entryPoints = context.options[0].entryPoints || ['ol']; + const relativeSourceRoot = context.options[0].root || 'src'; let gotFirst = false; return { CallExpression: function(expression) { if (gotFirst) { return; } - if (util.isProvideExpression(expression)) { + const isProvide = util.isProvideExpression(expression); + const isModule = util.isModuleExpression(expression); + + if (isProvide || isModule) { + const type = isProvide ? 'provide' : 'module'; gotFirst = true; const parent = expression.parent; if (parent.type !== 'ExpressionStatement') { - return context.report(expression, 'Expected goog.provide() to be in an expression statement'); + return context.report(expression, `Expected goog.${type}() to be in an expression statement`); } if (parent.parent.type !== 'Program') { - return context.report(expression, 'Expected goog.provide() to be at the top level'); + return context.report(expression, `Expected goog.${type}() to be at the top level`); } if (expression.arguments.length !== 1) { - return context.report(expression, 'Expected one argument for goog.provide()'); + return context.report(expression, `Expected one argument for goog.${type}()`); } const arg = expression.arguments[0]; if (arg.type !== 'Literal' || !arg.value || typeof arg.value !== 'string') { - return context.report(expression, 'Expected goog.provide() to be called with a string'); + return context.report(expression, `Expected goog.${type}() to be called with a string`); } const filePath = context.getFilename(); - const sourceRoot = path.join(process.cwd(), 'src'); + const sourceRoot = path.join(process.cwd(), relativeSourceRoot); const requirePath = path.relative(sourceRoot, filePath); const ext = '.js'; let name = arg.value; // special case for main entry point - if (name === 'ol') { + if (entryPoints.indexOf(name) !== -1) { name += '.index'; } const expectedPath = name.split('.').join(path.sep) + ext; if (expectedPath.toLowerCase() !== requirePath.toLowerCase()) { - return context.report(expression, `Expected goog.provide('${name}') to be like ${requirePath}`); + return context.report(expression, `Expected goog.${type}('${name}') to be like ${requirePath}`); } } } diff --git a/valid-requires.js b/valid-requires.js index 080ca5c..e037d4a 100644 --- a/valid-requires.js +++ b/valid-requires.js @@ -14,11 +14,14 @@ exports.rule = { CallExpression: function(expression) { if (util.isRequireExpression(expression)) { const parent = expression.parent; - if (parent.type !== 'ExpressionStatement') { - return context.report(expression, 'Expected goog.require() to be in an expression statement'); + const parentIsExpression = parent.type === 'ExpressionStatement'; + const parentIsVariableDeclarator = parent.type === 'VariableDeclarator'; + if (!parentIsExpression && !parentIsVariableDeclarator) { + return context.report(expression, 'Expected goog.require() to be in an expression or variable declarator statement'); } - if (parent.parent.type !== 'Program') { + const expectedProgram = parentIsExpression ? parent.parent : parent.parent.parent; + if (expectedProgram.type !== 'Program') { return context.report(expression, 'Expected goog.require() to be at the top level'); }