Skip to content

Chore: add require-meta-docs-url internal rule #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 18, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
'use strict'

const version = require('./package.json').version

module.exports = {
root: true,
parserOptions: {
Expand All @@ -15,8 +19,19 @@ module.exports = {
'eslint-plugin'
],
rules: {
'eslint-plugin/report-message-format': ['error', '^[A-Z].*\\.$'],
'eslint-plugin/report-message-format': ['error', '^[A-Z`\'].*\\.$'],
'eslint-plugin/prefer-placeholders': 'error',
'eslint-plugin/consistent-output': 'error'
}
},

overrides: [{
files: ['lib/rules/*.js'],
rules: {
"consistent-docs-description": "error",
"no-invalid-meta": "error",
"require-meta-docs-url": ["error", {
"pattern": `https://github.com/vuejs/eslint-plugin-vue/blob/v${version}/docs/rules/{{name}}.md`
}]
}
}]
}
244 changes: 244 additions & 0 deletions eslint-internal-rules/require-meta-docs-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* @author Teddy Katz <https://github.com/not-an-aardvark>
*
* Three functions `isNormalFunctionExpression`, `getKeyName`, and `getRuleInfo`
* are copied from https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/lib/utils.js
*
* I have a plan to send this rule to that plugin: https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/55
*/

'use strict'

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

const path = require('path')

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

/**
* Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression.
* @param {ASTNode} node The node in question
* @returns {boolean} `true` if the node is a normal function expression
*/
function isNormalFunctionExpression (node) {
return (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.generator && !node.async
}

/**
* Gets the key name of a Property, if it can be determined statically.
* @param {ASTNode} node The `Property` node
* @returns {string|null} The key name, or `null` if the name cannot be determined statically.
*/
function getKeyName (property) {
if (!property.computed && property.key.type === 'Identifier') {
return property.key.name
}
if (property.key.type === 'Literal') {
return '' + property.key.value
}
if (property.key.type === 'TemplateLiteral' && property.key.quasis.length === 1) {
return property.key.quasis[0].value.cooked
}
return null
}

/**
* Performs static analysis on an AST to try to determine the final value of `module.exports`.
* @param {ASTNode} ast The `Program` AST node
* @returns {Object} An object with keys `meta`, `create`, and `isNewStyle`. `meta` and `create` correspond to the AST nodes
for the final values of `module.exports.meta` and `module.exports.create`. `isNewStyle` will be `true` if `module.exports`
is an object, and `false` if module.exports is just the `create` function. If no valid ESLint rule info can be extracted
from the file, the return value will be `null`.
*/
function getRuleInfo (ast) {
const INTERESTING_KEYS = new Set(['create', 'meta'])
let exportsVarOverridden = false
let exportsIsFunction = false

const exportNodes = ast.body
.filter(statement => statement.type === 'ExpressionStatement')
.map(statement => statement.expression)
.filter(expression => expression.type === 'AssignmentExpression')
.filter(expression => expression.left.type === 'MemberExpression')
.reduce((currentExports, node) => {
if (
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
) {
exportsVarOverridden = true

if (isNormalFunctionExpression(node.right)) {
// Check `module.exports = function () {}`

exportsIsFunction = true
return { create: node.right, meta: null }
} else if (node.right.type === 'ObjectExpression') {
// Check `module.exports = { create: function () {}, meta: {} }`

exportsIsFunction = false
return node.right.properties.reduce((parsedProps, prop) => {
const keyValue = getKeyName(prop)
if (INTERESTING_KEYS.has(keyValue)) {
parsedProps[keyValue] = prop.value
}
return parsedProps
}, {})
}
return {}
} else if (
!exportsIsFunction &&
node.left.object.type === 'MemberExpression' &&
node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
) {
// Check `module.exports.create = () => {}`

currentExports[node.left.property.name] = node.right
} else if (
!exportsVarOverridden &&
node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
) {
// Check `exports.create = () => {}`

currentExports[node.left.property.name] = node.right
}
return currentExports
}, {})

return Object.prototype.hasOwnProperty.call(exportNodes, 'create') && isNormalFunctionExpression(exportNodes.create)
? Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes)
: null
}

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'require rules to implement a meta.docs.url property',
category: 'Rules',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
pattern: { type: 'string' }
},
additionalProperties: false
}]
},

/**
* Creates AST event handlers for require-meta-docs-url.
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const options = context.options[0] || {}
const sourceCode = context.getSourceCode()
const filename = context.getFilename()
const ruleName = filename === '<input>' ? undefined : path.basename(filename, '.js')
const expectedUrl = !options.pattern || !ruleName
? undefined
: options.pattern.replace(/{{\s*name\s*}}/g, ruleName)

/**
* Check whether a given node is the expected URL.
* @param {Node} node The node of property value to check.
* @returns {boolean} `true` if the node is the expected URL.
*/
function isExpectedUrl (node) {
return Boolean(
node &&
node.type === 'Literal' &&
typeof node.value === 'string' &&
(
expectedUrl === undefined ||
node.value === expectedUrl
)
)
}

/**
* Insert a given property into a given object literal.
* @param {SourceCodeFixer} fixer The fixer.
* @param {Node} node The ObjectExpression node to insert a property.
* @param {string} propertyText The property code to insert.
* @returns {void}
*/
function insertProperty (fixer, node, propertyText) {
if (node.properties.length === 0) {
return fixer.replaceText(node, `{\n${propertyText}\n}`)
}
return fixer.insertTextAfter(
sourceCode.getLastToken(node.properties[node.properties.length - 1]),
`,\n${propertyText}`
)
}

return {
Program (node) {
const info = getRuleInfo(node)
if (!info) {
return
}
const metaNode = info.meta
const docsPropNode =
metaNode &&
metaNode.properties &&
metaNode.properties.find(p => p.type === 'Property' && getKeyName(p) === 'docs')
const urlPropNode =
docsPropNode &&
docsPropNode.value.properties &&
docsPropNode.value.properties.find(p => p.type === 'Property' && getKeyName(p) === 'url')

if (isExpectedUrl(urlPropNode && urlPropNode.value)) {
return
}

context.report({
loc:
(urlPropNode && urlPropNode.value.loc) ||
(docsPropNode && docsPropNode.value.loc) ||
(metaNode && metaNode.loc) ||
node.loc.start,

message:
!urlPropNode ? 'Rules should export a `meta.docs.url` property.'
: !expectedUrl ? '`meta.docs.url` property must be a string.'
/* otherwise */ : '`meta.docs.url` property must be `{{expectedUrl}}`.',

data: {
expectedUrl
},

fix (fixer) {
if (expectedUrl) {
const urlString = JSON.stringify(expectedUrl)
if (urlPropNode) {
return fixer.replaceText(urlPropNode.value, urlString)
}
if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') {
return insertProperty(fixer, docsPropNode.value, `url: ${urlString}`)
}
if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') {
return insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`)
}
}
return null
}
})
}
}
}
}
6 changes: 0 additions & 6 deletions lib/rules/.eslintrc.json

This file was deleted.

90 changes: 44 additions & 46 deletions lib/rules/attribute-hyphenation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,56 +11,12 @@ const casing = require('../utils/casing')
// Rule Definition
// ------------------------------------------------------------------------------

function create (context) {
const sourceCode = context.getSourceCode()
const options = context.options[0]
const useHyphenated = options !== 'never'

const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')

function reportIssue (node, name) {
const text = sourceCode.getText(node.key)

context.report({
node: node.key,
loc: node.loc,
message: useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' cann't be hyphenated.",
data: {
text
},
fix: fixer => fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
})
}

function isIgnoredAttribute (value) {
if (value.indexOf('data-') !== -1 || value.indexOf('aria-') !== -1) {
return true
}
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return utils.defineTemplateBodyVisitor(context, {
VAttribute (node) {
if (!utils.isCustomComponent(node.parent.parent)) return

const name = !node.directive ? node.key.rawName : node.key.name === 'bind' ? node.key.raw.argument : false
if (!name || isIgnoredAttribute(name)) return

reportIssue(node, name)
}
})
}

module.exports = {
meta: {
docs: {
description: 'enforce attribute naming style in template',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/attribute-hyphenation.md'
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.1.0/docs/rules/attribute-hyphenation.md'
},
fixable: 'code',
schema: [
Expand All @@ -70,5 +26,47 @@ module.exports = {
]
},

create
create (context) {
const sourceCode = context.getSourceCode()
const options = context.options[0]
const useHyphenated = options !== 'never'

const caseConverter = casing.getConverter(useHyphenated ? 'kebab-case' : 'camelCase')

function reportIssue (node, name) {
const text = sourceCode.getText(node.key)

context.report({
node: node.key,
loc: node.loc,
message: useHyphenated ? "Attribute '{{text}}' must be hyphenated." : "Attribute '{{text}}' cann't be hyphenated.",
data: {
text
},
fix: fixer => fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
})
}

function isIgnoredAttribute (value) {
if (value.indexOf('data-') !== -1 || value.indexOf('aria-') !== -1) {
return true
}
return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
}

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return utils.defineTemplateBodyVisitor(context, {
VAttribute (node) {
if (!utils.isCustomComponent(node.parent.parent)) return

const name = !node.directive ? node.key.rawName : node.key.name === 'bind' ? node.key.raw.argument : false
if (!name || isIgnoredAttribute(name)) return

reportIssue(node, name)
}
})
}
}
Loading