UNPKG

eslint-plugin-san

Version:

Official ESLint plugin for San

211 lines (194 loc) 7.76 kB
/** * @author Toru Nagashima * @copyright 2016 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ 'use strict'; /* eslint-disable */ // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ const utils = require('../utils'); // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ /** * These strings wil be displayed in error messages. */ const ELEMENT_TYPE_MESSAGES = Object.freeze({ NORMAL: 'HTML elements', VOID: 'HTML void elements', COMPONENT: 'San.js custom components', SVG: 'SVG elements', MATH: 'MathML elements', UNKNOWN: 'unknown elements' }); /** * @typedef {object} Options * @property {'always' | 'never'} NORMAL * @property {'always' | 'never'} VOID * @property {'always' | 'never'} COMPONENT * @property {'always' | 'never'} SVG * @property {'always' | 'never'} MATH * @property {null} UNKNOWN */ /** * Normalize the given options. * @param {any} options The raw options object. * @returns {Options} Normalized options. */ function parseOptions(options) { return { NORMAL: (options && options.html && options.html.normal) || 'always', VOID: (options && options.html && options.html.void) || 'never', COMPONENT: (options && options.html && options.html.component) || 'always', SVG: (options && options.svg) || 'always', MATH: (options && options.math) || 'always', UNKNOWN: null }; } /** * Get the elementType of the given element. * @param {VElement} node The element node to get. * @returns {keyof Options} The elementType of the element. */ function getElementType(node) { if (utils.isCustomComponent(node)) { return 'COMPONENT'; } if (utils.isHtmlElementNode(node)) { if (utils.isHtmlVoidElementName(node.name)) { return 'VOID'; } return 'NORMAL'; } if (utils.isSvgElementNode(node)) { return 'SVG'; } if (utils.isMathMLElementNode(node)) { return 'MATH'; } return 'UNKNOWN'; } /** * Check whether the given element is empty or not. * This ignores whitespaces, doesn't ignore comments. * @param {VElement} node The element node to check. * @param {SourceCode} sourceCode The source code object of the current context. * @returns {boolean} `true` if the element is empty. */ function isEmpty(node, sourceCode) { const start = node.startTag.range[1]; const end = node.endTag != null ? node.endTag.range[0] : node.range[1]; return sourceCode.text.slice(start, end).trim() === ''; } // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { type: 'layout', docs: { description: 'enforce self-closing style', categories: ['strongly-recommended'], url: 'https://ecomfe.github.io/eslint-plugin-san/rules/html-self-closing.html' }, fixable: 'code', schema: { definitions: { optionValue: { enum: ['always', 'never', 'any'] } }, type: 'array', items: [ { type: 'object', properties: { html: { type: 'object', properties: { normal: {$ref: '#/definitions/optionValue'}, void: {$ref: '#/definitions/optionValue'}, component: {$ref: '#/definitions/optionValue'} }, additionalProperties: false }, svg: {$ref: '#/definitions/optionValue'}, math: {$ref: '#/definitions/optionValue'} }, additionalProperties: false } ], maxItems: 1 } }, /** @param {RuleContext} context */ create(context) { const sourceCode = context.getSourceCode(); const options = parseOptions(context.options[0]); let hasInvalidEOF = false; return utils.defineTemplateBodyVisitor( context, { VElement(node) { if (hasInvalidEOF) { return; } const elementType = getElementType(node); const mode = options[elementType]; if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) { context.report({ node, loc: node.loc, message: 'Require self-closing on {{elementType}} (<{{name}}>).', data: { elementType: ELEMENT_TYPE_MESSAGES[elementType], name: node.rawName }, fix(fixer) { const tokens = context.parserServices.getTemplateBodyTokenStore(); const close = tokens.getLastToken(node.startTag); if (close.type !== 'HTMLTagClose') { return null; } return fixer.replaceTextRange([close.range[0], node.range[1]], '/>'); } }); } if (mode === 'never' && node.startTag.selfClosing) { context.report({ node, loc: node.loc, message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).', data: { elementType: ELEMENT_TYPE_MESSAGES[elementType], name: node.rawName }, fix(fixer) { const tokens = context.parserServices.getTemplateBodyTokenStore(); const close = tokens.getLastToken(node.startTag); if (close.type !== 'HTMLSelfClosingTagClose') { return null; } if (elementType === 'VOID') { return fixer.replaceText(close, '>'); } // If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`, // so replace the entire element. // return fixer.replaceText(close, `></${node.rawName}>`) const elementPart = sourceCode.text.slice(node.range[0], close.range[0]); return fixer.replaceText(node, `${elementPart}></${node.rawName}>`); } }); } } }, { Program(node) { hasInvalidEOF = utils.hasInvalidEOF(node); } } ); } };