diff --git a/.eslintrc b/.eslintrc index 10d2238..efd3b41 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,13 @@ { + "extends": "eslint:recommended", + "plugins": [ + "filenames" + ], + "rules": { + "eol-last": 2, + "filenames/match-regex": [2, "^[a-z-]+$", true], + "filenames/match-exported": 2 + }, "env": { "node": true } diff --git a/.travis.yml b/.travis.yml index 5d065e4..1b927f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js sudo: false node_js: + - "5.5" + - "4.2" - "0.12" - "0.10" - "iojs" diff --git a/README.md b/README.md index d257d4e..98142b3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # eslint-plugin-filenames +__This project is no longer actively maintained__ + [![NPM Version](https://img.shields.io/npm/v/eslint-plugin-filenames.svg?style=flat-square)](https://www.npmjs.org/package/eslint-plugin-filenames) [![Build Status](https://img.shields.io/travis/selaux/eslint-plugin-filenames.svg?style=flat-square)](https://travis-ci.org/selaux/eslint-plugin-filenames) [![Coverage Status](https://img.shields.io/coveralls/selaux/eslint-plugin-filenames.svg?style=flat-square)](https://coveralls.io/r/selaux/eslint-plugin-filenames?branch=master) @@ -7,37 +9,150 @@ Adds [eslint](http://eslint.org/) rules to ensure consistent filenames for your javascript files. -__Please note__: This plugin will only lint the filenames of the `.js`-files you are linting with eslint. It will ignore all other files (e.g. non-js files, files not linted with eslint). +__Please note__: This plugin will only lint the filenames of the `.js`, `.jsx` files you are linting with eslint. It will ignore other files that are not linted with eslint. ## Enabling the plugin -Modify your `.eslintrc` file to load the plugin and enable the rule. +This plugin requires a version of `eslint>=1.0.0` to be installed as a peer dependency. -``` +Modify your `.eslintrc` file to load the plugin and enable the rules you want to use. + +```json { "plugins": [ "filenames" ], "rules": { - "filenames/filenames": 2 + "filenames/match-regex": 2, + "filenames/match-exported": 2, + "filenames/no-index": 2 } } ``` ## Rules -### Consistent Filenames (filenames) +### Consistent Filenames via regex (match-regex) + +A rule to enforce a certain file naming convention using a regular expression. + +The convention can be configured using a regular expression (the default is `camelCase.js`). Additionally +exporting files can be ignored with a second configuration parameter. + +```json +"filenames/match-regex": [2, "^[a-z_]+$", true] +``` + +With these configuration options, `camelCase.js` will be reported as an error while `snake_case.js` will pass. +Additionally the files that have a named default export (according to the logic in the `match-exported` rule) will be +ignored. They could be linted with the `match-exported` rule. Please note that exported function calls are not +respected in this case. + +### Matching Exported Values (match-exported) + +Match the file name against the default exported value in the module. Files that dont have a default export will +be ignored. The exports of `index.js` are matched against their parent directory. + +```js +// Considered problem only if the file isn't named foo.js or foo/index.js +export default function foo() {} + +// Considered problem only if the file isn't named Foo.js or Foo/index.js +module.exports = class Foo() {} + +// Considered problem only if the file isn't named someVariable.js or someVariable/index.js +module.exports = someVariable; + +// Never considered a problem +export default { foo: "bar" }; +``` + +If your filename policy doesn't quite match with your variable naming policy, you can add one or multiple transforms: + +```json +"filenames/match-exported": [ 2, "kebab" ] +``` + +Now, in your code: + +```js +// Considered problem only if file isn't named variable-name.js or variable-name/index.js +export default function variableName; +``` + +Available transforms: +'[snake](https://www.npmjs.com/package/lodash.snakecase)', +'[kebab](https://www.npmjs.com/package/lodash.kebabcase)', +'[camel](https://www.npmjs.com/package/lodash.camelcase)', and +'pascal' (camel-cased with first letter in upper case). -A rule to enforce a certain file naming convention. +For multiple transforms simply specify an array like this (null in this case stands for no transform): -The convention can be configured using a regular expression (the default is `camelCase.js`): +```json +"filenames/match-exported": [2, [ null, "kebab", "snake" ] ] +``` + +If you prefer to use suffixes for your files (e.g. `Foo.react.js` for a React component file), +you can use a second configuration parameter. It allows you to remove parts of a filename matching a regex pattern +before transforming and matching against the export. +```json +"filenames/match-exported": [ 2, null, "\\.react$" ] ``` -"filenames/filenames": [2, "^[a-z_]+$"] + +Now, in your code: + +```js +// Considered problem only if file isn't named variableName.react.js, variableName.js or variableName/index.js +export default function variableName; ``` +If you also want to match exported function calls you can use the third option (a boolean flag). + +```json +"filenames/match-exported": [ 2, null, null, true ] +``` + +Now, in your code: + +```js +// Considered problem only if file isn't named functionName.js or functionName/index.js +export default functionName(); +``` + +### Don't allow index.js files (no-index) + +Having a bunch of `index.js` files can have negative influence on developer experience, e.g. when +opening files by name. When enabling this rule. `index.js` files will always be considered a problem. + ## Changelog +#### 1.3.2 + +- Fix issue with `match-regex` and `getExportedName` + +#### 1.3.1 + +- Put breaking change from `1.3.0` behind a flag + +#### 1.3.0 + +- Support call expressions as named exports + +#### 1.2.0 +- Introduce `strip` option for `match-exported` +- Introduce support for multiple transform options +- Introduce `pascal` transform + +#### 1.1.0 +- Introduce `transform` option for `match-exported` + +#### 1.0.0 +- Split rule into `match-regex`, `match-exported` and `no-index` + +#### 0.2.0 +- Add match-exported flags + #### 0.1.2 - Fix example in README diff --git a/index.js b/index.js index 04d9ba9..e225f2c 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,8 @@ module.exports = { rules: { - filenames: require("./lib/rules/filenames") + "match-regex": require("./lib/rules/match-regex"), + "match-exported": require("./lib/rules/match-exported"), + "no-index": require("./lib/rules/no-index") } }; diff --git a/lib/common/getExportedName.js b/lib/common/getExportedName.js new file mode 100644 index 0000000..2848b2e --- /dev/null +++ b/lib/common/getExportedName.js @@ -0,0 +1,38 @@ +function getNodeName(node, options) { + var op = options || []; + + if (node.type === "Identifier") { + return node.name; + } + + if (node.id && node.id.type === "Identifier") { + return node.id.name; + } + + if (op[2] && node.type === "CallExpression" && node.callee.type === "Identifier") { + return node.callee.name; + } +} + +module.exports = function getExportedName(programNode, options) { + for (var i = 0; i < programNode.body.length; i += 1) { + var node = programNode.body[i]; + + // export default ... + if (node.type === "ExportDefaultDeclaration") { + return getNodeName(node.declaration, options); + } + + // module.exports = ... + if (node.type === "ExpressionStatement" && + node.expression.type === "AssignmentExpression" && + node.expression.left.type === "MemberExpression" && + node.expression.left.object.type === "Identifier" && + node.expression.left.object.name === "module" && + node.expression.left.property.type === "Identifier" && + node.expression.left.property.name === "exports" + ) { + return getNodeName(node.expression.right, options); + } + } +}; diff --git a/lib/common/isIgnoredFilename.js b/lib/common/isIgnoredFilename.js new file mode 100644 index 0000000..9662f03 --- /dev/null +++ b/lib/common/isIgnoredFilename.js @@ -0,0 +1,5 @@ +var ignoredFilenames = [ "", "" ]; + +module.exports = function isIgnoredFilename(filename) { + return ignoredFilenames.indexOf(filename) !== -1; +}; diff --git a/lib/common/isIndexFile.js b/lib/common/isIndexFile.js new file mode 100644 index 0000000..d8863df --- /dev/null +++ b/lib/common/isIndexFile.js @@ -0,0 +1,3 @@ +module.exports = function isIndexFile(parsed) { + return parsed.name === 'index'; +}; diff --git a/lib/common/parseFilename.js b/lib/common/parseFilename.js new file mode 100644 index 0000000..dc701c9 --- /dev/null +++ b/lib/common/parseFilename.js @@ -0,0 +1,12 @@ +var path = require('path'); + +module.exports = function parseFilename(filename) { + var ext = path.extname(filename); + + return { + dir: path.dirname(filename), + base: path.basename(filename), + ext: ext, + name: path.basename(filename, ext) + } +}; diff --git a/lib/rules/filenames.js b/lib/rules/filenames.js deleted file mode 100644 index 8a7fb61..0000000 --- a/lib/rules/filenames.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @fileoverview Rule to ensure that filenames match a convention (default: camelCase) - * @author Stefan Lau - */ - -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -var path = require("path"); -var ignoredFilenames = [ "", "" ]; - -module.exports = function(context) { - - "use strict"; - - var defaultRegexp = /^([a-z0-9]+)([A-Z][a-z0-9]+)*$/g, - conventionRegexp = context.options[0] ? new RegExp(context.options[0]) : defaultRegexp; - - //-------------------------------------------------------------------------- - // Helpers - //-------------------------------------------------------------------------- - - function matchesConvention(filenameWithoutExtension) { - return conventionRegexp.test(filenameWithoutExtension); - } - - function report(node, filename) { - context.report(node, "Filename '{{name}}' does not match the naming convention.", { name: filename }); - } - - return { - "Program": function(node) { - var filename = context.getFilename(), - extension = path.extname(filename), - isFromStdin = ignoredFilenames.indexOf(filename) !== -1, - filenameWithoutExtension = path.basename(filename, extension); - - if (!isFromStdin && !matchesConvention(filenameWithoutExtension)) { - report(node, filename); - } - } - }; - -}; diff --git a/lib/rules/match-exported.js b/lib/rules/match-exported.js new file mode 100644 index 0000000..31c9ac0 --- /dev/null +++ b/lib/rules/match-exported.js @@ -0,0 +1,123 @@ +/** + * @fileoverview Rule to ensure that filenames match the exports of the file + * @author Stefan Lau + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +var path = require('path'), + camelCase = require('lodash.camelcase'), + upperFirst = require('lodash.upperfirst'), + parseFilename = require('../common/parseFilename'), + isIgnoredFilename = require('../common/isIgnoredFilename'), + getExportedName = require('../common/getExportedName'), + isIndexFile = require('../common/isIndexFile'), + transforms = { + kebab: require('lodash.kebabcase'), + snake: require('lodash.snakecase'), + camel: camelCase, + pascal: function (name) { + return upperFirst(camelCase(name)); + } + }, + transformNames = Object.keys(transforms), + transformSchema = { "enum": transformNames.concat([ null ]) }; + +function getStringToCheckAgainstExport(parsed, replacePattern) { + var dirArray = parsed.dir.split(path.sep); + var lastDirectory = dirArray[dirArray.length - 1]; + + if (isIndexFile(parsed)) { + return lastDirectory; + } else { + return replacePattern ? parsed.name.replace(replacePattern, '') : parsed.name; + } +} + +function getTransformsFromOptions(option) { + var usedTransforms = (option && option.push) ? option : [ option ]; + + return usedTransforms.map(function (name) { + return transforms[name]; + }); +} + +function transform(exportedName, transforms) { + return transforms.map(function (t) { + return t ? t(exportedName) : exportedName; + }); +} + +function anyMatch(expectedExport, transformedNames) { + return transformedNames.some(function (name) { + return name === expectedExport; + }); +} + +function getWhatToMatchMessage(transforms) { + if (transforms.length === 1 && !transforms[0]) { + return "the exported name"; + } + if (transforms.length > 1) { + return "any of the exported and transformed names" + } + return "the exported and transformed name"; +} + +module.exports = function(context) { + return { + "Program": function (node) { + var transforms = getTransformsFromOptions(context.options[0]), + replacePattern = context.options[1] ? new RegExp(context.options[1]) : null, + filename = context.getFilename(), + absoluteFilename = path.resolve(filename), + parsed = parseFilename(absoluteFilename), + shouldIgnore = isIgnoredFilename(filename), + exportedName = getExportedName(node, context.options), + isExporting = Boolean(exportedName), + expectedExport = getStringToCheckAgainstExport(parsed, replacePattern), + transformedNames = transform(exportedName, transforms), + everythingIsIndex = exportedName === 'index' && parsed.name === 'index', + matchesExported = anyMatch(expectedExport, transformedNames) || everythingIsIndex, + reportIf = function (condition, messageForNormalFile, messageForIndexFile) { + var message = (!messageForIndexFile || !isIndexFile(parsed)) ? messageForNormalFile : messageForIndexFile; + + if (condition) { + context.report(node, message, { + name: parsed.base, + expectedExport: expectedExport, + exportName: transformedNames.join("', '"), + extension: parsed.ext, + whatToMatch: getWhatToMatchMessage(transforms) + }); + } + }; + + if (shouldIgnore) return; + + reportIf( + isExporting && !matchesExported, + "Filename '{{expectedExport}}' must match {{whatToMatch}} '{{exportName}}'.", + "The directory '{{expectedExport}}' must be named '{{exportName}}', after the exported value of its index file." + ); + } + } +}; + +module.exports.schema = [ + { + + oneOf: [ + transformSchema, + { type: "array", items: transformSchema, minItems: 1 } + ] + }, + { + type: [ "string", "null" ] + }, + { + type: [ "boolean", "null" ] + } +]; diff --git a/lib/rules/match-regex.js b/lib/rules/match-regex.js new file mode 100644 index 0000000..ebdf326 --- /dev/null +++ b/lib/rules/match-regex.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Rule to ensure that filenames match a convention (default: camelCase) + * @author Stefan Lau + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +"use strict"; + +var path = require("path"), + parseFilename = require('../common/parseFilename'), + getExportedName = require('../common/getExportedName'), + isIgnoredFilename = require('../common/isIgnoredFilename'); + +module.exports = function(context) { + var defaultRegexp = /^([a-z0-9]+)([A-Z][a-z0-9]+)*$/g, + conventionRegexp = context.options[0] ? new RegExp(context.options[0]) : defaultRegexp, + ignoreExporting = context.options[1] ? context.options[1] : false; + + return { + "Program": function(node) { + var filename = context.getFilename(), + absoluteFilename = path.resolve(filename), + parsed = parseFilename(absoluteFilename), + shouldIgnore = isIgnoredFilename(filename), + isExporting = Boolean(getExportedName(node)), + matchesRegex = conventionRegexp.test(parsed.name); + + if (shouldIgnore) return; + if (ignoreExporting && isExporting) return; + if (!matchesRegex) { + context.report(node, "Filename '{{name}}' does not match the naming convention.", { + name: parsed.base + }); + } + } + }; +}; diff --git a/lib/rules/no-index.js b/lib/rules/no-index.js new file mode 100644 index 0000000..54f3a62 --- /dev/null +++ b/lib/rules/no-index.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Rule to ensure that there exist no index files + * @author Stefan Lau + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +var path = require('path'), + parseFilename = require('../common/parseFilename'), + isIgnoredFilename = require('../common/isIgnoredFilename'), + isIndexFile = require('../common/isIndexFile'); + +module.exports = function(context) { + return { + "Program": function(node) { + var filename = context.getFilename(), + absoluteFilename = path.resolve(filename), + parsed = parseFilename(absoluteFilename), + shouldIgnore = isIgnoredFilename(filename), + isIndex = isIndexFile(parsed); + + + if (shouldIgnore) return; + if (isIndex) { + context.report(node, "'index.js' files are not allowed."); + } + } + }; + +}; diff --git a/package.json b/package.json index 42cdfcc..0c34e99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-filenames", - "version": "0.1.2", + "version": "1.3.2", "description": "Eslint rule for consistent filenames.", "main": "index.js", "scripts": { @@ -11,16 +11,23 @@ "report-coverage-html": "istanbul report --dir build/coverage html", "coveralls": "cat ./build/coverage/lcov.info | coveralls" }, + "dependencies": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + }, "devDependencies": { - "chai": "2.1.1", - "eslint-tester": "0.6.0", - "istanbul": "0.3.7", - "mocha": "2.2.1", - "eslint": ">=0.9.0", - "coveralls": "2.11.2" + "chai": "3.5.0", + "coveralls": "2.13.0", + "eslint": "^2.0.0", + "eslint-plugin-filenames": "^1.0.0", + "istanbul": "0.4.5", + "mocha": "3.2.0", + "sinon": "2.1.0" }, "peerDependencies": { - "eslint": ">=0.9.0" + "eslint": "*" }, "repository": { "type": "git", diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..b2916b9 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} \ No newline at end of file diff --git a/test/index.js b/test/index.js index c259ea3..5a2fd87 100644 --- a/test/index.js +++ b/test/index.js @@ -1,12 +1,21 @@ -var mocha = require("mocha"), - expect = require("chai").expect, +var expect = require("chai").expect, index = require("../index.js"), - rule = require("../lib/rules/filenames"); + matchRegex = require("../lib/rules/match-regex"), + matchExported = require("../lib/rules/match-exported"), + noIndex = require("../lib/rules/no-index"); -mocha.describe("index.js", function () { +describe("index.js", function () { "use strict"; - mocha.it("should export the filenames rule", function () { - expect(index.rules.filenames).to.equal(rule); + it("should export the match-regex rule", function () { + expect(index.rules['match-regex']).to.equal(matchRegex); + }); + + it("should export the match-exported rule", function () { + expect(index.rules['match-exported']).to.equal(matchExported); + }); + + it("should export the no-index rule", function () { + expect(index.rules['no-index']).to.equal(noIndex); }); }); diff --git a/test/rules/filenames.js b/test/rules/filenames.js deleted file mode 100644 index 26ca66f..0000000 --- a/test/rules/filenames.js +++ /dev/null @@ -1,69 +0,0 @@ -var linter = require("eslint").linter, - ESLintTester = require("eslint-tester"); - -var eslintTester = new ESLintTester(linter), - testCode = "var foo = 'bar';"; - -eslintTester.addRuleTest("lib/rules/filenames", { - - valid: [ - { - code: testCode, - filename: "" - }, - { - code: testCode, - filename: "" - }, - { - code: testCode, - filename: "foobar.js" - }, - { - code: testCode, - filename: "fooBar.js" - }, - { - code: testCode, - filename: "foo1Bar1.js" - }, - { - code: testCode, - filename: "foo_bar.js", - args: [ 1, "^[a-z_]+$" ] - } - ], - - invalid: [ - { - code: testCode, - filename: "foo_bar.js", - errors: [ - { message: "Filename 'foo_bar.js' does not match the naming convention.", column: 0, line: 0 } - ] - }, - { - code: testCode, - filename: "fooBAR.js", - errors: [ - { message: "Filename 'fooBAR.js' does not match the naming convention.", column: 0, line: 0 } - ] - }, - { - code: testCode, - filename: "fooBar$.js", - errors: [ - { message: "Filename 'fooBar$.js' does not match the naming convention.", column: 0, line: 0 } - ] - }, - { - code: testCode, - filename: "fooBar.js", - args: [ 1, "^[a-z_]$" ], - errors: [ - { message: "Filename \'fooBar.js\' does not match the naming convention.", column: 0, line: 0 } - ] - } - ] - -}); diff --git a/test/rules/match-exported.js b/test/rules/match-exported.js new file mode 100644 index 0000000..867b670 --- /dev/null +++ b/test/rules/match-exported.js @@ -0,0 +1,371 @@ +var exportedRule = require("../../lib/rules/match-exported"), + RuleTester = require("eslint").RuleTester; + +var testCode = "var foo = 'bar';", + testCallCode = "export default foo();", + exportedVariableCode = "module.exports = exported;", + exportedJsxClassCode = "module.exports = class Foo { render() { return Test Class; } };", + exportedClassCode = "module.exports = class Foo {};", + exportedFunctionCode = "module.exports = function foo() {};", + exportUnnamedFunctionCode = "module.exports = function() {};", + exportedCalledFunctionCode = "module.exports = foo();", + exportedJsxFunctionCode = "module.exports = function foo() { return Test Fn };", + exportedEs6VariableCode = "export default exported;", + exportedEs6ClassCode = "export default class Foo {};", + exportedEs6JsxClassCode = "export default class Foo { render() { return Test Class; } };", + exportedEs6FunctionCode = "export default function foo() {};", + exportedEs6JsxFunctionCode = "export default function foo() { return Test Fn };", + exportedEs6Index = "export default function index() {};", + camelCaseCommonJS = "module.exports = variableName;", + snakeCaseCommonJS = "module.exports = variable_name;", + camelCaseEs6 = "export default variableName;", + snakeCaseEs6 = "export default variable_name;", + ruleTester = new RuleTester(); + +ruleTester.run("lib/rules/match-exported", exportedRule, { + valid: [ + { + code: testCode, + filename: "" + }, + { + code: testCode, + filename: "" + }, + { + code: exportUnnamedFunctionCode, + filename: "testFile.js" + }, + { + code: testCode, + filename: "/some/dir/exported.js" + }, + { + code: testCallCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedVariableCode, + filename: "/some/dir/exported.js" + }, + { + code: exportedClassCode, + filename: "/some/dir/Foo.js", + parserOptions: { ecmaVersion: 6 } + }, + { + code: exportedJsxClassCode, + filename: "/some/dir/Foo.js", + parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } + }, + { + code: exportedFunctionCode, + filename: "/some/dir/foo.js" + }, + { + code: exportedCalledFunctionCode, + filename: "/some/dir/bar.js" + }, + { + code: exportedJsxFunctionCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaFeatures: { jsx: true } } + }, + { + code: exportedEs6VariableCode, + filename: "/some/dir/exported.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedEs6ClassCode, + filename: "/some/dir/Foo.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedEs6JsxClassCode, + filename: "/some/dir/Foo.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } } + }, + { + code: exportedEs6FunctionCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedEs6JsxFunctionCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } } + }, + { + code: exportedEs6FunctionCode, + filename: "/some/dir/foo/index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedEs6JsxFunctionCode, + filename: "/some/dir/foo/index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } } + }, + { + code: exportedEs6FunctionCode, + // /foo is used as cwd for test setup so full path will be /foo/index.js + filename: "index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: exportedEs6Index, + // /foo is used as cwd for test setup so full path will be /foo/index.js + filename: "index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" } + } + ], + + invalid: [ + { + code: exportedVariableCode, + filename: "/some/dir/fooBar.js", + errors: [ + { message: "Filename 'fooBar' must match the exported name 'exported'.", column: 1, line: 1 } + ] + }, + { + code: exportedClassCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaVersion: 6 }, + errors: [ + { message: "Filename 'foo' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedJsxClassCode, + filename: "/some/dir/foo.js", + parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, + errors: [ + { message: "Filename 'foo' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedFunctionCode, + filename: "/some/dir/bar.js", + errors: [ + { message: "Filename 'bar' must match the exported name 'foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedJsxFunctionCode, + filename: "/some/dir/bar.js", + parserOptions: { ecmaFeatures: { jsx: true } }, + errors: [ + { message: "Filename 'bar' must match the exported name 'foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6VariableCode, + filename: "/some/dir/fooBar.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { message: "Filename 'fooBar' must match the exported name 'exported'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6ClassCode, + filename: "/some/dir/bar.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { message: "Filename 'bar' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6JsxClassCode, + filename: "/some/dir/bar.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }, + errors: [ + { message: "Filename 'bar' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6FunctionCode, + filename: "/some/dir/fooBar/index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { message: "The directory 'fooBar' must be named 'foo', after the exported value of its index file.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6JsxFunctionCode, + filename: "/some/dir/fooBar/index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }, + errors: [ + { message: "The directory 'fooBar' must be named 'foo', after the exported value of its index file.", column: 1, line: 1 } + ] + }, + { + code: exportedVariableCode, + filename: "index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { message: "The directory 'foo' must be named 'exported', after the exported value of its index file.", column: 1, line: 1 } + ] + }, + { + code: exportedJsxClassCode, + filename: "/some/dir/Foo.react.js", + parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, + errors: [ + { message: "Filename 'Foo.react' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + } + ] +}); + +ruleTester.run("lib/rules/match-exported with configuration", exportedRule, { + valid: [ + { + code: camelCaseCommonJS, + filename: "variable_name.js", + options: ['snake'] + }, + { + code: camelCaseCommonJS, + filename: "variable_name/index.js", + options: ['snake'] + }, + { + code: camelCaseCommonJS, + filename: "variable-name.js", + options: ['kebab'] + }, + { + code: snakeCaseCommonJS, + filename: "variableName.js", + options: ['camel'] + }, + { + code: camelCaseEs6, + filename: "variable_name.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['snake'] + }, + { + code: camelCaseEs6, + filename: "variable-name.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['kebab'] + }, + { + code: snakeCaseEs6, + filename: "variableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['camel'] + }, + { + code: snakeCaseEs6, + filename: "VariableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['pascal'] + }, + { + code: snakeCaseEs6, + filename: "variableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: [ [ 'pascal', 'camel' ] ] + }, + { + code: snakeCaseEs6, + filename: "VariableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: [ [ 'pascal', 'camel' ] ] + }, + { + code: exportedJsxClassCode, + filename: "/some/dir/Foo.react.js", + parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, + options: [null, "\\.react$"] + }, + { + code: exportedEs6JsxClassCode, + filename: "/some/dir/Foo.react.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }, + options: [null, "\\.react$"] + }, + { + code: exportedCalledFunctionCode, + filename: "/some/dir/foo.js", + options: [null, null, true] + } + ], + + invalid: [ + { + code: camelCaseCommonJS, + filename: "variableName.js", + options: ['snake'], + errors: [ + { message: "Filename 'variableName' must match the exported and transformed name 'variable_name'.", column: 1, line: 1 } + ] + }, + { + code: camelCaseEs6, + filename: "variableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['kebab'], + errors: [ + { message: "Filename 'variableName' must match the exported and transformed name 'variable-name'.", column: 1, line: 1 } + ] + }, + { + code: camelCaseEs6, + filename: "variableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: ['pascal'], + errors: [ + { message: "Filename 'variableName' must match the exported and transformed name 'VariableName'.", column: 1, line: 1 } + ] + }, + { + code: camelCaseEs6, + filename: "VariableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: [ [ null ] ], + errors: [ + { message: "Filename 'VariableName' must match the exported name 'variableName'.", column: 1, line: 1 } + ] + }, + { + code: camelCaseEs6, + filename: "variableName.js", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + options: [ ['pascal', 'snake' ] ], + errors: [ + { message: "Filename 'variableName' must match any of the exported and transformed names 'VariableName', 'variable_name'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6JsxClassCode, + filename: "/some/dir/Foo.bar.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }, + options: [null, "\\.react$"], + errors: [ + { message: "Filename 'Foo.bar' must match the exported name 'Foo'.", column: 1, line: 1 } + ] + }, + { + code: exportedEs6JsxClassCode, + filename: "/some/dir/Foo.react/index.js", + parserOptions: { ecmaVersion: 6, sourceType: "module", ecmaFeatures: { jsx: true } }, + options: [null, "\\.react$"], + errors: [ + { message: "The directory 'Foo.react' must be named 'Foo', after the exported value of its index file.", column: 1, line: 1 } + ] + }, + { + code: exportedCalledFunctionCode, + filename: "/some/dir/bar.js", + errors: [ + { message: "Filename 'bar' must match the exported name 'foo'.", column: 1, line: 1 } + ], + options: [null, null, true] + } + ] +}); diff --git a/test/rules/match-regex.js b/test/rules/match-regex.js new file mode 100644 index 0000000..74bdad7 --- /dev/null +++ b/test/rules/match-regex.js @@ -0,0 +1,93 @@ +var regexRule = require("../../lib/rules/match-regex"), + RuleTester = require("eslint").RuleTester; + +var exportingCode = 'module.exports = foo', + exportedFunctionCall = 'module.exports = foo()', + testCode = "var foo = 'bar';", + ruleTester = new RuleTester(); + +ruleTester.run("lib/rules/match-regex", regexRule, { + valid: [ + { + code: testCode, + filename: "" + }, + { + code: testCode, + filename: "" + }, + { + code: testCode, + filename: "foobar.js" + }, + { + code: testCode, + filename: "fooBar.js" + }, + { + code: testCode, + filename: "foo1Bar1.js" + }, + { + code: testCode, + filename: "foo_bar.js", + options: [ "^[a-z_]+$" ] + }, + { + code: testCode, + filename: "/foo/dir/foo_bar.js", + options: [ "^[a-z_]+$" ] + }, + { + code: testCode, + filename: "/foo/dir/fooBar.js" + }, + { + code: exportingCode, + filename: "foo_bar.js", + options: [ null, true ] + }, + { + code: exportingCode, + filename: "fooBar.js", + options: [ "^[a-z_]$", true ] + }, + { + code: exportedFunctionCall, + filename: "foo_bar.js", + options: [ "^[a-z_]+$", true ] + } + ], + + invalid: [ + { + code: testCode, + filename: "/some/dir/foo_bar.js", + errors: [ + { message: "Filename 'foo_bar.js' does not match the naming convention.", column: 1, line: 1 } + ] + }, + { + code: testCode, + filename: "/some/dir/fooBAR.js", + errors: [ + { message: "Filename 'fooBAR.js' does not match the naming convention.", column: 1, line: 1 } + ] + }, + { + code: testCode, + filename: "fooBar$.js", + errors: [ + { message: "Filename 'fooBar$.js' does not match the naming convention.", column: 1, line: 1 } + ] + }, + { + code: testCode, + filename: "fooBar.js", + options: [ "^[a-z_]$" ], + errors: [ + { message: "Filename 'fooBar.js' does not match the naming convention.", column: 1, line: 1 } + ] + } + ] +}); diff --git a/test/rules/no-index.js b/test/rules/no-index.js new file mode 100644 index 0000000..e036f62 --- /dev/null +++ b/test/rules/no-index.js @@ -0,0 +1,43 @@ +var indexRule = require("../../lib/rules/no-index"), + RuleTester = require("eslint").RuleTester; + +var testCode = "var foo = 'bar';", + ruleTester = new RuleTester(); + +ruleTester.run("lib/rules/no-index", indexRule, { + valid: [ + { + code: testCode, + filename: "" + }, + { + code: testCode, + filename: "" + }, + { + code: testCode, + filename: "foo.js" + }, + { + code: testCode, + filename: "/some/dir/foo.js" + } + ], + + invalid: [ + { + code: testCode, + filename: "index.js", + errors: [ + { message: "'index.js' files are not allowed.", column: 1, line: 1 } + ] + }, + { + code: testCode, + filename: "/some/dir/index.js", + errors: [ + { message: "'index.js' files are not allowed.", column: 1, line: 1 } + ] + } + ] +}); diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..be0288d --- /dev/null +++ b/test/setup.js @@ -0,0 +1,9 @@ +var sinon = require("sinon"); + +beforeEach(function () { + sinon.stub(process, "cwd").returns('/foo'); +}); + +afterEach(function () { + process.cwd.restore(); +});