From f1da865ecaabbebbbba6cf0171762400bbf11d38 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Thu, 1 Oct 2015 10:14:19 +0200 Subject: [PATCH] refactor(): Make Meteor file metadata available to rules Rules now get the Meteor root directory passed to them. They can use that information to extract further information about the file being linted based on its path in the project. --- lib/index.js | 24 ++- lib/rules/audit-argument-checks.js | 2 +- lib/rules/no-blaze-lifecycle-assignment.js | 2 +- lib/rules/no-session.js | 16 +- lib/rules/no-zero-timeout.js | 2 +- lib/util/environment.js | 10 ++ lib/util/folderNames.js | 10 ++ lib/util/getMeteorMeta.js | 107 ++++++++++++ lib/util/getMeteorProjectRootPath.js | 19 +++ package.json | 19 ++- tests/index.js | 6 +- tests/lib/rules/audit-argument-checks.js | 2 +- .../rules/no-blaze-lifecycle-assignment.js | 2 +- tests/lib/rules/no-session.js | 39 ++++- tests/lib/rules/no-zero-timeout.js | 2 +- tests/lib/util/environment.js | 9 ++ tests/lib/util/folderNames.js | 9 ++ tests/lib/util/getMeteorMeta.js | 153 ++++++++++++++++++ tests/lib/util/getMeteorProjectRootPath.js | 32 ++++ 19 files changed, 439 insertions(+), 26 deletions(-) create mode 100644 lib/util/environment.js create mode 100644 lib/util/folderNames.js create mode 100644 lib/util/getMeteorMeta.js create mode 100644 lib/util/getMeteorProjectRootPath.js create mode 100644 tests/lib/util/environment.js create mode 100644 tests/lib/util/folderNames.js create mode 100644 tests/lib/util/getMeteorMeta.js create mode 100644 tests/lib/util/getMeteorProjectRootPath.js diff --git a/lib/index.js b/lib/index.js index c10b298b32..6a4ea3dc12 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,25 @@ +import getMeteorProjectRootPath from './util/getMeteorProjectRootPath.js' + +// const getMeteorProjectRootPath = require('./util/getMeteorProjectRootPath.js'); + +const cwd = process.cwd() +const rootPath = getMeteorProjectRootPath(cwd) + +function unpack (rule) { + const packedRule = require(rule) + const unpackedRule = packedRule(rootPath) + Object.keys(packedRule).map(function (key) { + unpackedRule[key] = packedRule[key] + }) + return unpackedRule +} + module.exports = { rules: { - 'no-session': require('./rules/no-session'), - 'no-blaze-lifecycle-assignment': require('./rules/no-blaze-lifecycle-assignment'), - 'no-zero-timeout': require('./rules/no-zero-timeout'), - 'audit-argument-checks': require('./rules/audit-argument-checks') + 'no-session': unpack('./rules/no-session'), + 'no-blaze-lifecycle-assignment': unpack('./rules/no-blaze-lifecycle-assignment'), + 'no-zero-timeout': unpack('./rules/no-zero-timeout'), + 'audit-argument-checks': unpack('./rules/audit-argument-checks') }, rulesConfig: { 'no-session': 0, diff --git a/lib/rules/audit-argument-checks.js b/lib/rules/audit-argument-checks.js index 02b4cce8b3..f06c9eadcc 100644 --- a/lib/rules/audit-argument-checks.js +++ b/lib/rules/audit-argument-checks.js @@ -8,7 +8,7 @@ // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // --------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/no-blaze-lifecycle-assignment.js b/lib/rules/no-blaze-lifecycle-assignment.js index 8766e819c0..3eaf271bce 100644 --- a/lib/rules/no-blaze-lifecycle-assignment.js +++ b/lib/rules/no-blaze-lifecycle-assignment.js @@ -7,7 +7,7 @@ // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // --------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/no-session.js b/lib/rules/no-session.js index 4ed5baedca..d4981e0eb9 100644 --- a/lib/rules/no-session.js +++ b/lib/rules/no-session.js @@ -3,15 +3,22 @@ * @author Dominik Ferber */ +// import getMeteorMeta from '../util/getMeteorMeta.js' + // ----------------------------------------------------------------------------- // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = (/* rootPath */) => context => { + // const fileInfo = getMeteorMeta(rootPath, context.getFilename()) + // console.log(fileInfo) - // ------------------------------------------------------------------------- - // Public - // ------------------------------------------------------------------------- + // fileInfo is false => not in Meteor Project + // fileInfo => {env, path, isCompatibilityFile} + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- return { @@ -22,7 +29,6 @@ module.exports = function (context) { } } - } module.exports.schema = [] diff --git a/lib/rules/no-zero-timeout.js b/lib/rules/no-zero-timeout.js index 5189432969..21149bbea3 100644 --- a/lib/rules/no-zero-timeout.js +++ b/lib/rules/no-zero-timeout.js @@ -7,7 +7,7 @@ // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // ------------------------------------------------------------------------- // Public diff --git a/lib/util/environment.js b/lib/util/environment.js new file mode 100644 index 0000000000..35775c7aa7 --- /dev/null +++ b/lib/util/environment.js @@ -0,0 +1,10 @@ +export default { + PUBLIC: '-public', + PRIVATE: '-private', + CLIENT: '-client', + SERVER: '-server', + PACKAGE: '-pkg', + TEST: '-test', + NODE_MODULE: '-node_module', + UNIVERSAL: '-universal' +} diff --git a/lib/util/folderNames.js b/lib/util/folderNames.js new file mode 100644 index 0000000000..db9b488522 --- /dev/null +++ b/lib/util/folderNames.js @@ -0,0 +1,10 @@ +export default { + PUBLIC: 'public', + PRIVATE: 'private', + CLIENT: 'client', + SERVER: 'server', + TESTS: 'tests', + PACKAGES: 'packages', + NODE_MODULES: 'node_modules', + COMPATIBILITY: 'compatibility' +} diff --git a/lib/util/getMeteorMeta.js b/lib/util/getMeteorMeta.js new file mode 100644 index 0000000000..916b4992c2 --- /dev/null +++ b/lib/util/getMeteorMeta.js @@ -0,0 +1,107 @@ +import path from 'path' +import invariant from 'invariant' +import ENVIRONMENT from './environment.js' +import folderNames from './folderNames.js' + +function matchFirst (dirs, list) { + for (let i = 0; i < dirs.length; i++) { + if (list.indexOf(dirs[i]) !== -1) { + return dirs[i] + } + } + return false +} + +function isCompatibilityMode (pathList) { + var clientIndex = pathList.indexOf(folderNames.CLIENT) + + // file is directly in client-folder, so it can't be in COMPATIBILITY + if (pathList.length - 2 === clientIndex) { + return false + } + + return pathList[clientIndex + 1] === folderNames.COMPATIBILITY +} + +function determineEnvironment (pathList) { + + if (pathList[0] === folderNames.PUBLIC) { + return ENVIRONMENT.PUBLIC + } + + if (pathList[0] === folderNames.PRIVATE) { + return ENVIRONMENT.PRIVATE + } + + if (pathList.length > 2 && pathList[0] === folderNames.PACKAGES) { + return ENVIRONMENT.PACKAGE + } + + const specialFolders = [ + folderNames.CLIENT, + folderNames.SERVER, + folderNames.TESTS, + folderNames.NODE_MODULES + ] + + // remove filename + const dirList = pathList.slice(0, -1) + const matchedEnvironment = matchFirst(dirList, specialFolders) + + switch (matchedEnvironment) { + case folderNames.CLIENT: + return ENVIRONMENT.CLIENT + case folderNames.SERVER: + return ENVIRONMENT.SERVER + case folderNames.TESTS: + return ENVIRONMENT.TEST + case folderNames.NODE_MODULES: + return ENVIRONMENT.NODE_MODULE + default: + return ENVIRONMENT.UNIVERSAL + } + +} + +function stripPathPrefix (parent, child) { + const normalizedParent = path.normalize(parent) + const normalizedChild = path.normalize(child) + + invariant( + normalizedChild.substr(0, normalizedParent.length) === parent, + 'Linted file is not in CWD' + ) + + // also strip the / at the end, which is not in normalizedParent + return normalizedChild.substr(normalizedParent.length + 1) +} + +function getMeteorFileInfo (rootPath, filename) { + const pathInProject = stripPathPrefix(rootPath, filename) + const pathList = pathInProject.split(path.sep) + const environment = determineEnvironment(pathList) + + return { + path: pathInProject, + env: environment, + isCompatibilityFile: environment === ENVIRONMENT.CLIENT && isCompatibilityMode(pathList), + isInMeteorProject: true + } +} + +export default function getMeteorMeta (rootPath, filename) { + + if (typeof rootPath === 'object') { + + // rule is in test-mode. return the given environment + return rootPath + } + + if (!rootPath) { + + // not in a Meteor Project + return {isInMeteorProject: false} + } + + return getMeteorFileInfo(rootPath, filename) +} diff --git a/lib/util/getMeteorProjectRootPath.js b/lib/util/getMeteorProjectRootPath.js new file mode 100644 index 0000000000..6a28933af2 --- /dev/null +++ b/lib/util/getMeteorProjectRootPath.js @@ -0,0 +1,19 @@ +import path from 'path' + +// must be require for rewire to work +var pathExists = require('path-exists') + +export default function getMeteorProjectRootPath (currentDirectory) { + + // No folder with '.meteor/release' in it found + if (currentDirectory === path.sep) { + return false + } + + const meteorPath = path.join(currentDirectory, '.meteor', 'release') + if (pathExists.sync(meteorPath)) { + return currentDirectory + } + + return getMeteorProjectRootPath(path.join(currentDirectory, '..')) +} diff --git a/package.json b/package.json index 6b5414f5c8..269eb77a61 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "description": "Meteor specific linting rules for ESLint", "main": "dist/index.js", "scripts": { - "build": "babel lib -d dist", + "build": "babel lib -d dist --auxiliary-comment-before \"istanbul ignore next\"", "build:w": "npm run build -- -w", "clean": "rimraf dist", "coveralls": "cat ./reports/coverage/lcov.info | coveralls", "lint": "eslint ./", "prebuild": "npm run clean && mkdir dist", "prepublish": "npm run build", - "preunit-test": "npm run build", "semantic-release": "semantic-release pre && npm publish && semantic-release post", - "test": "npm run lint && npm run unit-test", - "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot --compilers js:babel/register" + "test": "npm run lint && npm run build && npm run unit-test", + "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot --compilers js:babel/register", + "unit-test:n": "node_modules/mocha/bin/_mocha tests/**/*.js --recursive --reporter dot --compilers js:babel/register" }, "files": [ "LICENSE", @@ -27,18 +27,23 @@ }, "homepage": "https://github.com/dferber90/eslint-plugin-meteor", "bugs": "https://github.com/dferber90/eslint-plugin-meteor/issues", - "dependencies": {}, + "dependencies": { + "invariant": "2.1.1", + "path-exists": "2.0.0" + }, "devDependencies": { "babel": "5.8.23", + "babel-eslint": "4.1.3", "coveralls": "2.11.4", "cz-conventional-changelog": "1.0.1", "eslint": "1.5.1", - "babel-eslint": "4.1.3", "ghooks": "0.3.2", "istanbul": "0.3.21", "mocha": "2.3.3", + "rewire": "2.3.4", + "rimraf": "2.4.3", "semantic-release": "4.3.5", - "rimraf": "2.4.3" + "validate-commit-msg": "1.0.0" }, "peerDependencies": { "eslint": ">=0.8.0" diff --git a/tests/index.js b/tests/index.js index bdfaeaa22d..953e2f2a10 100755 --- a/tests/index.js +++ b/tests/index.js @@ -18,10 +18,10 @@ var defaultSettings = {} describe('all rule files should be exported by the plugin', function() { rules.forEach(function(ruleName) { - it('should export ' + ruleName, function() { + it('should export ' + ruleName, function () { assert.equal( - plugin.rules[ruleName], - require(path.join('../dist/rules', ruleName)) + typeof plugin.rules[ruleName], + 'function' ) }) diff --git a/tests/lib/rules/audit-argument-checks.js b/tests/lib/rules/audit-argument-checks.js index 6f47be51d8..80993d4f78 100644 --- a/tests/lib/rules/audit-argument-checks.js +++ b/tests/lib/rules/audit-argument-checks.js @@ -16,7 +16,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('audit-argument-checks', rule, { +ruleTester.run('audit-argument-checks', rule(), { valid: [ 'foo()', diff --git a/tests/lib/rules/no-blaze-lifecycle-assignment.js b/tests/lib/rules/no-blaze-lifecycle-assignment.js index b39b4caa98..c071aba49e 100644 --- a/tests/lib/rules/no-blaze-lifecycle-assignment.js +++ b/tests/lib/rules/no-blaze-lifecycle-assignment.js @@ -18,7 +18,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('no-blaze-lifecycle-assignment', rule, { +ruleTester.run('no-blaze-lifecycle-assignment', rule(), { valid: [ 'x += 1', diff --git a/tests/lib/rules/no-session.js b/tests/lib/rules/no-session.js index 8bd5672539..e20feabe1f 100644 --- a/tests/lib/rules/no-session.js +++ b/tests/lib/rules/no-session.js @@ -9,16 +9,53 @@ // Requirements // ----------------------------------------------------------------------------- +import {SERVER, CLIENT} from '../../../dist/util/environment.js' var rule = require('../../../dist/rules/no-session') var RuleTester = require('eslint').RuleTester +// ----------------------------------------------------------------------------- +// Environments +// ----------------------------------------------------------------------------- + +const serverEnv = { + path: 'server/methods.js', + env: SERVER, + isCompatibilityFile: false, + isInMeteorProject: true +} + +const clientEnv = { + path: 'server/methods.js', + env: CLIENT, + isCompatibilityFile: false, + isInMeteorProject: true +} + + // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- + var ruleTester = new RuleTester() -ruleTester.run('no-session', rule, { +ruleTester.run('no-session', rule(serverEnv), { + + valid: [ + 'session.get("foo")', + 'foo(Session)' + ], + + invalid: [ + {code: 'Session.set("foo", true)', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.get("foo")', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.clear("foo")', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.all()', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]} + ] + +}) + +ruleTester.run('no-session', rule(clientEnv), { valid: [ 'session.get("foo")', diff --git a/tests/lib/rules/no-zero-timeout.js b/tests/lib/rules/no-zero-timeout.js index 53c33524b9..f7ffcb6274 100644 --- a/tests/lib/rules/no-zero-timeout.js +++ b/tests/lib/rules/no-zero-timeout.js @@ -16,7 +16,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('no-zero-timeout', rule, { +ruleTester.run('no-zero-timeout', rule(), { valid: [ 'Meteor.setTimeout()', diff --git a/tests/lib/util/environment.js b/tests/lib/util/environment.js new file mode 100644 index 0000000000..490d4748f9 --- /dev/null +++ b/tests/lib/util/environment.js @@ -0,0 +1,9 @@ +/* eslint-env mocha */ + +var assert = require('assert') + +describe('environment', function () { + it('is defined', function () { + assert.ok(require('../../../dist/util/environment.js')) + }) +}) diff --git a/tests/lib/util/folderNames.js b/tests/lib/util/folderNames.js new file mode 100644 index 0000000000..75615c9777 --- /dev/null +++ b/tests/lib/util/folderNames.js @@ -0,0 +1,9 @@ +/* eslint-env mocha */ + +var assert = require('assert') + +describe('folder names', function () { + it('is defined', function () { + assert.ok(require('../../../dist/util/folderNames.js')) + }) +}) diff --git a/tests/lib/util/getMeteorMeta.js b/tests/lib/util/getMeteorMeta.js new file mode 100644 index 0000000000..ddd5a308b6 --- /dev/null +++ b/tests/lib/util/getMeteorMeta.js @@ -0,0 +1,153 @@ +/* eslint-env mocha */ + +var assert = require('assert') +var path = require('path') + +var ENVIRONMENT = require('../../../dist/util/environment.js') +var getMeteorMeta = require('../../../dist/util/getMeteorMeta.js') + + +const rootPath = path.join('User', 'anon', 'meteor-project') + +describe('getMeteorMeta', function () { + it('has working rule test-mode', function () { + var someObj = {} + assert.equal(getMeteorMeta(someObj), someObj) + }) + + describe('when not in Meteor project', function () { + it('returns default env', function () { + var result = getMeteorMeta(false, 'file.js') + assert.equal(typeof result, 'object') + assert.equal(result.isInMeteorProject, false) + assert.equal(Object.keys(result).length, 1) + }) + }) + + describe('in public', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'public', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'public/file.js') + assert.equal(result.env, ENVIRONMENT.PUBLIC) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('in private', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'private', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'private/file.js') + assert.equal(result.env, ENVIRONMENT.PRIVATE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('in package', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'packages', 'awesome-pkg', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'packages/awesome-pkg/file.js') + assert.equal(result.env, ENVIRONMENT.PACKAGE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on no special folder', function () { + it('has universal environment', function () { + var filename = path.join(rootPath, 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'file.js') + assert.equal(result.env, ENVIRONMENT.UNIVERSAL) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on client', function () { + + it('returns file info', function () { + var filename = path.join(rootPath, 'client', 'lib', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/lib/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + it('does not detect compatibility when directly in client-folder ', function () { + var filename = path.join(rootPath, 'client', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + it('detects compatibility mode', function () { + var filename = path.join(rootPath, 'client', 'compatibility', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/compatibility/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, true) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on server', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'server', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'server/file.js') + assert.equal(result.env, ENVIRONMENT.SERVER) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + describe('that is nested', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'lib', 'server', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'lib/server/file.js') + assert.equal(result.env, ENVIRONMENT.SERVER) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + }) + + describe('in tests', function () { + var filename = path.join(rootPath, 'tests', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'tests/file.js') + assert.equal(result.env, ENVIRONMENT.TEST) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + describe('in node_modules', function () { + var filename = path.join(rootPath, 'node_modules', 'my-module', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'node_modules/my-module/file.js') + assert.equal(result.env, ENVIRONMENT.NODE_MODULE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + +}) diff --git a/tests/lib/util/getMeteorProjectRootPath.js b/tests/lib/util/getMeteorProjectRootPath.js new file mode 100644 index 0000000000..0abfa2c90a --- /dev/null +++ b/tests/lib/util/getMeteorProjectRootPath.js @@ -0,0 +1,32 @@ +/* eslint-env mocha */ + +var assert = require('assert') +var path = require('path') +var rewire = require('rewire') +var getMeteorProjectRootPath = rewire('../../../dist/util/getMeteorProjectRootPath.js') + + +var isInMeteorProject +getMeteorProjectRootPath.__set__('pathExists', { + sync: function () { + return isInMeteorProject + } +}) + + +describe('getMeteorProjectRootPath', function () { + it('returns false for top-level directory', function () { + assert.equal(getMeteorProjectRootPath(path.sep), false) + }) + + it('returns false when not in meteor project', function () { + isInMeteorProject = false + assert.equal(getMeteorProjectRootPath('/not-in-meteor/sub'), false) + }) + + it('returns the directory when in meteor project', function () { + isInMeteorProject = true + var meteorPath = '/Users/anon/meteor-project' + assert.equal(getMeteorProjectRootPath(meteorPath), meteorPath) + }) +})