diff --git a/lib/internal-rules/.eslintrc.yml b/lib/internal-rules/.eslintrc.yml new file mode 100644 index 00000000000..22d3a30ce32 --- /dev/null +++ b/lib/internal-rules/.eslintrc.yml @@ -0,0 +1,3 @@ +rules: + internal-no-invalid-meta: "error" + internal-consistent-docs-description: "error" diff --git a/lib/internal-rules/internal-consistent-docs-description.js b/lib/internal-rules/internal-consistent-docs-description.js new file mode 100644 index 00000000000..3e4671aa7b1 --- /dev/null +++ b/lib/internal-rules/internal-consistent-docs-description.js @@ -0,0 +1,131 @@ +/** + * @fileoverview Internal rule to enforce meta.docs.description conventions. + * @author Vitor Balocco + */ + +"use strict"; + +const ALLOWED_FIRST_WORDS = [ + "enforce", + "require", + "disallow" +]; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the property of the Object node passed in that has the name specified. + * + * @param {string} property Name of the property to return. + * @param {ASTNode} node The ObjectExpression node. + * @returns {ASTNode} The Property node or null if not found. + */ +function getPropertyFromObject(property, node) { + const properties = node.properties; + + for (let i = 0; i < properties.length; i++) { + if (properties[i].key.name === property) { + return properties[i]; + } + } + + return null; +} + +/** + * Verifies that the meta.docs.description property follows our internal conventions. + * + * @param {RuleContext} context The ESLint rule context. + * @param {ASTNode} exportsNode ObjectExpression node that the rule exports. + * @returns {void} + */ +function checkMetaDocsDescription(context, exportsNode) { + if (exportsNode.type !== "ObjectExpression") { + + // if the exported node is not the correct format, "internal-no-invalid-meta" will already report this. + return; + } + + const metaProperty = getPropertyFromObject("meta", exportsNode); + const metaDocs = metaProperty && getPropertyFromObject("docs", metaProperty.value); + const metaDocsDescription = metaDocs && getPropertyFromObject("description", metaDocs.value); + + if (!metaDocsDescription) { + + // if there is no `meta.docs.description` property, "internal-no-invalid-meta" will already report this. + return; + } + + const description = metaDocsDescription.value.value; + + if (typeof description !== "string") { + context.report({ + node: metaDocsDescription.value, + message: "`meta.docs.description` should be a string." + }); + return; + } + + if (description === "") { + context.report({ + node: metaDocsDescription.value, + message: "`meta.docs.description` should not be empty.", + }); + return; + } + + if (description.indexOf(" ") === 0) { + context.report({ + node: metaDocsDescription.value, + message: "`meta.docs.description` should not start with whitespace." + }); + return; + } + + const firstWord = description.split(" ")[0]; + + if (ALLOWED_FIRST_WORDS.indexOf(firstWord) === -1) { + context.report({ + node: metaDocsDescription.value, + message: "`meta.docs.description` should start with one of the following words: {{ allowedWords }}. Started with \"{{ firstWord }}\" instead.", + data: { + allowedWords: ALLOWED_FIRST_WORDS.join(", "), + firstWord + } + }); + return; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce correct conventions of `meta.docs.description` property in core rules", + category: "Internal", + recommended: false + }, + + schema: [] + }, + + create(context) { + return { + AssignmentExpression(node) { + if (node.left && + node.right && + node.left.type === "MemberExpression" && + node.left.object.name === "module" && + node.left.property.name === "exports") { + + checkMetaDocsDescription(context, node.right); + } + } + }; + } +}; diff --git a/lib/rules/.eslintrc.yml b/lib/rules/.eslintrc.yml index fded5b84978..22d3a30ce32 100644 --- a/lib/rules/.eslintrc.yml +++ b/lib/rules/.eslintrc.yml @@ -1,2 +1,3 @@ rules: internal-no-invalid-meta: "error" + internal-consistent-docs-description: "error" diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js index cbbd1daf8e1..e42375d6bcc 100644 --- a/lib/rules/sort-keys.js +++ b/lib/rules/sort-keys.js @@ -1,5 +1,5 @@ /** - * @fileoverview Rule to requires object keys to be sorted + * @fileoverview Rule to require object keys to be sorted * @author Toru Nagashima */ diff --git a/tests/lib/internal-rules/internal-consistent-docs-description.js b/tests/lib/internal-rules/internal-consistent-docs-description.js new file mode 100644 index 00000000000..de32c71a901 --- /dev/null +++ b/tests/lib/internal-rules/internal-consistent-docs-description.js @@ -0,0 +1,264 @@ +/** + * @fileoverview Tests for internal-consistent-docs-description rule. + * @author Vitor Balocco + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/internal-rules/internal-consistent-docs-description"), + RuleTester = require("../../../lib/testers/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run("internal-consistent-docs-description", rule, { + valid: [ + + // wrong exports format: "internal-no-invalid-meta" reports this already + [ + "module.exports = function(context) {", + " return {", + " Program: function(node) {}", + " };", + "};" + ].join("\n"), + + // missing `meta.docs.description` property: "internal-no-invalid-meta" reports this already + [ + "module.exports = {", + " meta: {},", + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'enforce some stuff'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'require some things'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'disallow bad things'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n") + ], + invalid: [ + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'do stuff'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should start with one of the following words: enforce, require, disallow. Started with \"do\" instead.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'Require stuff'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should start with one of the following words: enforce, require, disallow. Started with \"Require\" instead.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'Enforce stuff'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should start with one of the following words: enforce, require, disallow. Started with \"Enforce\" instead.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: 'Disallow stuff'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should start with one of the following words: enforce, require, disallow. Started with \"Disallow\" instead.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: ' disallow (whitespace in the beginning)'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should not start with whitespace.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: ' disallow (whitespaces in the beginning)'", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should not start with whitespace.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: ' '", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should not start with whitespace.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: ''", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should not be empty.", + line: 4, + column: 26 + }] + }, + { + code: [ + "module.exports = {", + " meta: {", + " docs: {", + " description: true", + " }", + " },", + + " create: function(context) {", + " return {};", + " }", + "};" + ].join("\n"), + errors: [{ + message: "`meta.docs.description` should be a string.", + line: 4, + column: 26 + }] + } + ] +});