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.
This commit is contained in:
Dominik Ferber
2015-10-01 10:14:19 +02:00
parent 07610a906a
commit f1da865eca
19 changed files with 439 additions and 26 deletions

View File

@@ -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,

View File

@@ -8,7 +8,7 @@
// -----------------------------------------------------------------------------
module.exports = function (context) {
module.exports = () => (context) => {
// ---------------------------------------------------------------------------
// Helpers

View File

@@ -7,7 +7,7 @@
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = function (context) {
module.exports = () => (context) => {
// ---------------------------------------------------------------------------
// Helpers

View File

@@ -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 = []

View File

@@ -7,7 +7,7 @@
// Rule Definition
// -----------------------------------------------------------------------------
module.exports = function (context) {
module.exports = () => (context) => {
// -------------------------------------------------------------------------
// Public

10
lib/util/environment.js Normal file
View File

@@ -0,0 +1,10 @@
export default {
PUBLIC: '-public',
PRIVATE: '-private',
CLIENT: '-client',
SERVER: '-server',
PACKAGE: '-pkg',
TEST: '-test',
NODE_MODULE: '-node_module',
UNIVERSAL: '-universal'
}

10
lib/util/folderNames.js Normal file
View File

@@ -0,0 +1,10 @@
export default {
PUBLIC: 'public',
PRIVATE: 'private',
CLIENT: 'client',
SERVER: 'server',
TESTS: 'tests',
PACKAGES: 'packages',
NODE_MODULES: 'node_modules',
COMPATIBILITY: 'compatibility'
}

107
lib/util/getMeteorMeta.js Normal file
View File

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

View File

@@ -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, '..'))
}

View File

@@ -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"

View File

@@ -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'
)
})

View File

@@ -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()',

View File

@@ -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',

View File

@@ -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")',

View File

@@ -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()',

View File

@@ -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'))
})
})

View File

@@ -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'))
})
})

View File

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

View File

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