Merge pull request #6277 from tschaub/unambiguous-path

Name modules more like their provide
This commit is contained in:
Tim Schaub
2016-12-18 14:39:08 -07:00
committed by GitHub
30 changed files with 1 additions and 481 deletions

View File

@@ -55,7 +55,7 @@
"debounce": "^1.0.0",
"eslint": "3.12.1",
"eslint-config-openlayers": "6.0.0",
"eslint-plugin-openlayers-internal": "2.2.0",
"eslint-plugin-openlayers-internal": "^3.0.0",
"expect.js": "0.3.1",
"gaze": "^1.0.0",
"istanbul": "0.4.5",

View File

@@ -1,8 +0,0 @@
{
"env": {
"es6": true
},
"rules": {
"strict": [2, "global"]
}
}

View File

@@ -1,13 +0,0 @@
'use strict';
module.exports = {
rules: {
'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,
'requires-first': require('./requires-first').rule,
'valid-provide': require('./valid-provide').rule,
'valid-requires': require('./valid-requires').rule
}
};

View File

@@ -1,48 +0,0 @@
'use strict';
const util = require('./util');
exports.rule = {
meta: {
docs: {
description: 'disallow duplicate goog.require() calls'
},
fixable: 'code'
},
create: function(context) {
const alreadyRequired = {};
return {
ExpressionStatement: function(statement) {
if (util.isRequireStatement(statement)) {
const expression = statement.expression;
if (!expression.arguments[0]) {
return;
}
const name = expression.arguments[0].value;
if (alreadyRequired[name]) {
const source = context.getSourceCode();
return context.report({
node: statement,
message: `Duplicate goog.require('${name}')`,
fix: function(fixer) {
const afterToken = source.getTokenAfter(statement);
const range = [
statement.range[0],
afterToken ? afterToken.range[0] : statement.range[1]
];
return fixer.removeRange(range);
}
});
}
alreadyRequired[name] = true;
}
}
};
}
};

View File

@@ -1,94 +0,0 @@
'use strict';
const util = require('./util');
/**
* Unfortunately fragile RegExp to follow. Here is the logic:
*
* 1. check if a name looks like a const (ol.foo.BOO_HOO must have a "_")
* if so, require the "namespace" (ol.foo)
* 2. check if a name looks like a class (ol.foo.Bar or ol.foo.XYZ)
* if so, require the class (ol.foo.Bar)
* 3. otherwise, unless it's an external dependency (ol.ext.*), lop off the last
* part of a name and require the rest (e.g. ol.foo.bar would require ol.foo,
* 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: {
docs: {
description: 'ensure there are goog.require() calls for all used symbols'
},
fixable: 'code'
},
create: function(context) {
const defined = {};
return {
ExpressionStatement: function(statement) {
if (util.isRequireStatement(statement) || util.isProvideStatement(statement)) {
const expression = statement.expression;
const arg = expression.arguments[0];
if (!arg || !arg.value) {
return;
}
defined[arg.value] = true;
}
},
MemberExpression: function(expression) {
const parent = expression.parent;
if (parent.type !== 'MemberExpression') {
const name = util.getName(expression);
if (name && name.startsWith('ol.')) {
// check if the name looks like a const
let match = name.match(CONST_RE);
if (match) {
if (!defined[match[1]]) {
context.report(expression, `Missing goog.require('${match[1]}')`);
}
return;
}
// check if the name looks like a class
match = name.match(CLASS_RE);
if (match) {
const className = match[1];
const parts = className.split('.');
const lastPart = parts[parts.length - 1];
if (lastPart.toUpperCase() === lastPart) {
// unfortunately ambiguous:
// ol.has.WEBGL -> require('ol.has')
// ol.source.XYZ -> require('ol.source.XYZ')
const objectName = parts.slice(0, -1).join('.');
if (!defined[className] && !defined[objectName]) {
context.report(expression, `Missing goog.require('${className}') or goog.require('${objectName}')`);
}
return;
}
if (!defined[className]) {
context.report(expression, `Missing goog.require('${className}')`);
}
return;
}
// otherwise, assume the object should be required
const parts = name.split('.');
if (parts[1] !== 'ext') {
// unless it's an ol.ext.*
parts.pop();
}
const objectName = parts.join('.');
if (!defined[objectName]) {
context.report(expression, `Missing goog.require('${objectName}')`);
}
}
}
}
};
}
};

View File

@@ -1,90 +0,0 @@
'use strict';
const util = require('./util');
exports.rule = {
meta: {
docs: {
description: 'disallow unused goog.require() calls'
},
fixable: 'code'
},
create: function(context) {
// a lookup of goog.require() nodes by argument
const requireStatements = {};
// used names from member expressions that match the goog.require() arg
const usedNames = {};
return {
ExpressionStatement: function(statement) {
if (util.isRequireStatement(statement)) {
const expression = statement.expression;
const arg = expression.arguments[0];
if (!arg || !arg.value) {
return;
}
const name = arg.value;
requireStatements[name] = statement;
}
},
MemberExpression: function(node) {
const name = util.getName(node);
if (name in requireStatements) {
const requiredAncestor = context.getAncestors().some(
ancestorNode => !!requireStatements[util.getName(ancestorNode)]);
if (!requiredAncestor) {
usedNames[name] = true;
}
}
},
Identifier: function(node) {
const name = node.name;
if (name in requireStatements) {
const ancestors = context.getAncestors();
if (ancestors.length && ancestors[0].type === 'MemberExpression') {
const requiredAncestor = ancestors.some(
ancestorNode => !!requireStatements[util.getName(ancestorNode)]);
if (!requiredAncestor) {
usedNames[name] = true;
}
} else {
usedNames[name] = true;
}
}
},
'Program:exit': function(node) {
const source = context.getSourceCode();
for (let name in requireStatements) {
if (!usedNames[name]) {
const unusedRequire = requireStatements[name];
context.report({
node: unusedRequire,
message: `Unused ${source.getText(unusedRequire)}`,
fix: function(fixer) {
const afterToken = source.getTokenAfter(unusedRequire);
const range = [
unusedRequire.range[0],
afterToken ? afterToken.range[0] : unusedRequire.range[1]
];
return fixer.removeRange(range);
}
});
}
}
}
};
}
};

View File

@@ -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;
}
}
}
};
}
};

View File

@@ -1,11 +0,0 @@
{
"name": "eslint-plugin-openlayers-internal",
"version": "2.2.0",
"description": "Custom ESLint rules for the OpenLayers project",
"main": "index.js",
"repository": {
"type": "git",
"url": "git://github.com/openlayers/ol3.git"
},
"license": "BSD-2-Clause"
}

View File

@@ -1,30 +0,0 @@
'use strict';
const util = require('./util');
exports.rule = {
meta: {
docs: {
description: 'require that all goog.require() precede other statements (except goog.provide())'
}
},
create: function(context) {
return {
Program: function(program) {
let otherSeen = false;
program.body.forEach(statement => {
if (util.isRequireStatement(statement)) {
if (otherSeen) {
return context.report(statement, 'Expected goog.require() to precede other statements');
}
} else if (!util.isProvideStatement(statement)) {
otherSeen = true;
}
});
}
};
}
};

View File

@@ -1,49 +0,0 @@
'use strict';
function isGoogCallExpression(node, name) {
const callee = node.callee;
return callee && callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' && callee.object.name === 'goog' &&
callee.property.type === 'Identifier' && !callee.property.computed &&
callee.property.name === name;
}
function isGoogStatement(node, name) {
return node.expression && node.expression.type === 'CallExpression' &&
isGoogCallExpression(node.expression, name);
}
exports.isProvideExpression = function(node) {
return isGoogCallExpression(node, 'provide');
};
exports.isProvideStatement = function(node) {
return isGoogStatement(node, 'provide');
};
exports.isRequireExpression = function(node) {
return isGoogCallExpression(node, 'require');
};
exports.isRequireStatement = function(node) {
return isGoogStatement(node, 'require');
};
var getName = exports.getName = function(node) {
if (node.type !== 'MemberExpression') {
return;
}
if (node.property.type !== 'Identifier' || node.property.computed) {
return;
}
let objectName;
if (node.object.type === 'Identifier' && !node.object.computed) {
objectName = node.object.name;
} else if (node.object.type === 'MemberExpression' && !node.object.computed) {
objectName = getName(node.object);
}
if (!objectName) {
return;
}
return `${objectName}.${node.property.name}`;
};

View File

@@ -1,72 +0,0 @@
'use strict';
const path = require('path');
const util = require('./util');
function longestCommonPrefix(path1, path2) {
const parts1 = path.resolve(path1).split(path.sep);
const parts2 = path.resolve(path2).split(path.sep);
const common = [];
for (let i = 0, ii = parts1.length; i < ii; ++i) {
if (parts1[i] === parts2[i]) {
common.push(parts1[i]);
} else {
break;
}
}
return common.join(path.sep);
}
exports.rule = {
meta: {
docs: {
description: 'require the first goog.provide() has an arg named like the file path'
}
},
create: function(context) {
let gotFirst = false;
return {
CallExpression: function(expression) {
if (gotFirst) {
return;
}
if (util.isProvideExpression(expression)) {
gotFirst = true;
const parent = expression.parent;
if (parent.type !== 'ExpressionStatement') {
return context.report(expression, 'Expected goog.provide() to in an expression statement');
}
if (parent.parent.type !== 'Program') {
return context.report(expression, 'Expected goog.provide() to be at the top level');
}
if (expression.arguments.length !== 1) {
return context.report(expression, 'Expected one argument for goog.require()');
}
const arg = expression.arguments[0];
if (arg.type !== 'Literal' || !arg.value || typeof arg.value !== 'string') {
return context.report(expression, 'Expected goog.require() to be called with a string');
}
const filePath = context.getFilename();
const sourceRoot = path.join(longestCommonPrefix(__dirname, filePath), 'src');
const requirePath = path.relative(sourceRoot, filePath);
let ext;
if (path.basename(requirePath) === 'index.js') {
ext = path.sep + 'index.js';
} else {
ext = '.js';
}
const name = arg.value;
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}`);
}
}
}
};
}
};

View File

@@ -1,37 +0,0 @@
'use strict';
const util = require('./util');
exports.rule = {
meta: {
docs: {
description: 'require that all goog.require() have a valid arg and appear at the top level'
}
},
create: function(context) {
return {
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');
}
if (parent.parent.type !== 'Program') {
return context.report(expression, 'Expected goog.require() to be at the top level');
}
if (expression.arguments.length !== 1) {
return context.report(expression, 'Expected one argument for goog.require()');
}
const arg = expression.arguments[0];
if (arg.type !== 'Literal' || !arg.value || typeof arg.value !== 'string') {
return context.report(expression, 'Expected goog.require() to be called with a string');
}
}
}
};
}
};