UNPKG

@d0whc3r/eslint-plugin-stencil

Version:

ESLint rules specific to Stencil JS projects.

1,438 lines (1,411 loc) 69.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var ts = require('typescript'); var ts__default = _interopDefault(ts); var eslintUtils = require('eslint-utils'); var tsutils = require('tsutils'); var os = require('os'); const SyntaxKind = ts__default.SyntaxKind; function isPrivate(originalNode) { if (originalNode.modifiers) { return originalNode.modifiers.some(m => (m.kind === ts__default.SyntaxKind.PrivateKeyword || m.kind === ts__default.SyntaxKind.ProtectedKeyword)); } return false; } function getDecorator(node, decoratorName) { if (decoratorName) { return node.decorators && node.decorators.find(isDecoratorNamed(decoratorName)); } return node.decorators ? node.decorators.filter((dec) => dec.expression) : []; } function parseDecorator(decorator) { if (decorator && decorator.expression && decorator.expression.type === 'CallExpression') { return decorator.expression.arguments.map((a) => { const parsed = eslintUtils.getStaticValue(a); return parsed ? parsed.value : undefined; }); } return []; } function decoratorName(dec) { return dec && dec.expression && dec.expression.callee && dec.expression.callee.name; } function isDecoratorNamed(propName) { return (dec) => { return decoratorName(dec) === propName; }; } function stencilComponentContext() { let componentNode; return { rules: { 'ClassDeclaration': (node) => { const component = getDecorator(node, 'Component'); if (component) { const [{ tag }] = parseDecorator(component); if (tag) { componentNode = component; } } }, 'ClassDeclaration:exit': (node) => { if (componentNode === node) { componentNode = undefined; } } }, isComponent() { return !!componentNode; } }; } function getType(node) { return node.typeAnnotation.typeAnnotation.typeName.name; } const stencilDecorators = ['Component', 'Prop', 'State', 'Watch', 'Element', 'Method', 'Event', 'Listen']; const stencilLifecycle = [ 'connectedCallback', 'componentWillLoad', 'componentDidLoad', 'componentWillRender', 'componentDidRender', 'componentShouldUpdate', 'componentWillUpdate', 'componentDidUpdate', 'componentDidUnload', 'disconnectedCallback', 'render' ]; const TOKEN_TO_TEXT = { [SyntaxKind.OpenBraceToken]: '{', [SyntaxKind.CloseBraceToken]: '}', [SyntaxKind.OpenParenToken]: '(', [SyntaxKind.CloseParenToken]: ')', [SyntaxKind.OpenBracketToken]: '[', [SyntaxKind.CloseBracketToken]: ']', [SyntaxKind.DotToken]: '.', [SyntaxKind.DotDotDotToken]: '...', [SyntaxKind.SemicolonToken]: ',', [SyntaxKind.CommaToken]: ',', [SyntaxKind.LessThanToken]: '<', [SyntaxKind.GreaterThanToken]: '>', [SyntaxKind.LessThanEqualsToken]: '<=', [SyntaxKind.GreaterThanEqualsToken]: '>=', [SyntaxKind.EqualsEqualsToken]: '==', [SyntaxKind.ExclamationEqualsToken]: '!=', [SyntaxKind.EqualsEqualsEqualsToken]: '===', [SyntaxKind.InstanceOfKeyword]: 'instanceof', [SyntaxKind.ExclamationEqualsEqualsToken]: '!==', [SyntaxKind.EqualsGreaterThanToken]: '=>', [SyntaxKind.PlusToken]: '+', [SyntaxKind.MinusToken]: '-', [SyntaxKind.AsteriskToken]: '*', [SyntaxKind.AsteriskAsteriskToken]: '**', [SyntaxKind.SlashToken]: '/', [SyntaxKind.PercentToken]: '%', [SyntaxKind.PlusPlusToken]: '++', [SyntaxKind.MinusMinusToken]: '--', [SyntaxKind.LessThanLessThanToken]: '<<', [SyntaxKind.LessThanSlashToken]: '</', [SyntaxKind.GreaterThanGreaterThanToken]: '>>', [SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>', [SyntaxKind.AmpersandToken]: '&', [SyntaxKind.BarToken]: '|', [SyntaxKind.CaretToken]: '^', [SyntaxKind.ExclamationToken]: '!', [SyntaxKind.TildeToken]: '~', [SyntaxKind.AmpersandAmpersandToken]: '&&', [SyntaxKind.BarBarToken]: '||', [SyntaxKind.QuestionToken]: '?', [SyntaxKind.ColonToken]: ':', [SyntaxKind.EqualsToken]: '=', [SyntaxKind.PlusEqualsToken]: '+=', [SyntaxKind.MinusEqualsToken]: '-=', [SyntaxKind.AsteriskEqualsToken]: '*=', [SyntaxKind.AsteriskAsteriskEqualsToken]: '**=', [SyntaxKind.SlashEqualsToken]: '/=', [SyntaxKind.PercentEqualsToken]: '%=', [SyntaxKind.LessThanLessThanEqualsToken]: '<<=', [SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=', [SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=', [SyntaxKind.AmpersandEqualsToken]: '&=', [SyntaxKind.BarEqualsToken]: '|=', [SyntaxKind.CaretEqualsToken]: '^=', [SyntaxKind.AtToken]: '@', [SyntaxKind.InKeyword]: 'in', [SyntaxKind.UniqueKeyword]: 'unique', [SyntaxKind.KeyOfKeyword]: 'keyof', [SyntaxKind.NewKeyword]: 'new', [SyntaxKind.ImportKeyword]: 'import', [SyntaxKind.ReadonlyKeyword]: 'readonly' }; const rule = { meta: { docs: { description: 'This rule catches Stencil public methods that are not async.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; const typeChecker = parserServices.program.getTypeChecker(); return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition > Decorator[expression.callee.name=Method]': (decoratorNode) => { if (!stencil.isComponent()) { return; } const node = decoratorNode.parent; const method = parserServices.esTreeNodeToTSNodeMap.get(node); const signature = typeChecker.getSignatureFromDeclaration(method); const returnType = typeChecker.getReturnTypeOfSignature(signature); if (!tsutils.isThenableType(typeChecker, method, returnType)) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const text = String(originalNode.getFullText()); context.report({ node: node.key, message: `External @Method() ${node.key.name}() must return a Promise. Consider prefixing the method with async, such as @Method() async ${node.key.name}().`, fix(fixer) { const result = text.replace('@Method()\n', '@Method()\nasync') .replace('@Method() ', '@Method() async') .replace('async public', 'public async') .replace('async private', 'private async'); return fixer.replaceText(node, result); } }); } } }); } }; const DEFAULTS = ['stencil', 'stnl', 'st']; const rule$1 = { meta: { docs: { description: 'This rule catches usages banned prefix in component tag name.', category: 'Possible Errors', recommended: true }, schema: [ { type: 'array', items: { type: 'string' }, minLength: 1, additionalProperties: false } ], type: 'problem' }, create(context) { const stencil = stencilComponentContext(); return Object.assign(Object.assign({}, stencil.rules), { 'ClassDeclaration': (node) => { const component = getDecorator(node, 'Component'); if (!component) { return; } const [opts] = parseDecorator(component); if (!opts || !opts.tag) { return; } const tag = opts.tag; const options = context.options[0] || DEFAULTS; const match = options.some((t) => tag.startsWith(t)); if (match) { context.report({ node: node, message: `The component with tag name ${tag} have a banned prefix.` }); } } }); } }; const rule$2 = { meta: { docs: { description: 'This rule catches usages of non valid class names.', category: 'Possible Errors', recommended: false }, schema: [ { type: 'object', properties: { pattern: { type: 'string' }, ignoreCase: { type: 'boolean' } }, additionalProperties: false } ], type: 'problem' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'ClassDeclaration': (node) => { const component = getDecorator(node, 'Component'); const options = context.options[0]; const { pattern, ignoreCase } = options || {}; if (!component || !options || !pattern) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const className = originalNode.symbol.escapedName; const regExp = new RegExp(pattern, ignoreCase ? 'i' : undefined); if (!regExp.test(className)) { const [opts] = parseDecorator(component); if (!opts || !opts.tag) { return; } context.report({ node: node, message: `The class name in component with tag name ${opts.tag} is not valid (${regExp}).` }); } } }); } }; const rule$3 = { meta: { docs: { description: 'This rule catches Stencil Decorators used in incorrect locations.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { const stencil = stencilComponentContext(); return Object.assign(Object.assign({}, stencil.rules), { 'Decorator': (node) => { if (!stencil.isComponent()) { return; } if (node.expression && node.expression.callee) { const decName = node.expression.callee.name; if (decName === 'Prop' || decName === 'State' || decName === 'Element' || decName === 'Event') { if (node.parent.type !== 'ClassProperty') { context.report({ node: node, message: `The @${decName} decorator can only be applied to class properties.` }); } } else if (decName === 'Method' || decName === 'Watch' || decName === 'Listen') { if (node.parent.type !== 'MethodDefinition') { context.report({ node: node, message: `The @${decName} decorator can only be applied to class methods.` }); } } else if (decName === 'Component') { if (node.parent.type !== 'ClassDeclaration') { context.report({ node: node, message: `The @${decName} decorator can only be applied to a class.` }); } } } } }); } }; const ENUMERATE = ['inline', 'multiline', 'ignore']; const DEFAULTS$1 = { prop: 'ignore', state: 'ignore', element: 'ignore', event: 'ignore', method: 'ignore', watch: 'ignore', listen: 'ignore' }; const rule$4 = { meta: { docs: { description: 'This rule catches Stencil Decorators not used in consistent style.', category: 'Possible Errors', recommended: true }, schema: [ { type: 'object', properties: { prop: { type: 'string', enum: ENUMERATE }, state: { type: 'string', enum: ENUMERATE }, element: { type: 'string', enum: ENUMERATE }, event: { type: 'string', enum: ENUMERATE }, method: { type: 'string', enum: ENUMERATE }, watch: { type: 'string', enum: ENUMERATE }, listen: { type: 'string', enum: ENUMERATE } } } ], fixable: 'code', type: 'layout' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; const opts = context.options[0] || {}; const options = Object.assign(Object.assign({}, DEFAULTS$1), opts); function checkStyle(decorator) { const decName = decoratorName(decorator); const config = options[decName.toLowerCase()]; if (!config || config === 'ignore') { return; } const decoratorNode = parserServices.esTreeNodeToTSNodeMap.get(decorator); const decoratorText = decoratorNode.getText() .replace('(', '\\(') .replace(')', '\\)'); const text = decoratorNode.parent.getText(); const separator = config === 'multiline' ? '\\r?\\n' : ' '; const regExp = new RegExp(`${decoratorText}${separator}`, 'i'); if (!regExp.test(text)) { const node = decorator.parent; context.report({ node: node, message: `The @${decName} decorator can only be applied as ${config}.`, fix(fixer) { const opposite = config === 'multiline' ? ' ' : '\\r?\\n'; const separatorChar = config === 'multiline' ? os.EOL : ' '; const matchRegExp = new RegExp(`(${decoratorText})([${opposite}]+)`, 'i'); const result = text.replace(matchRegExp, `$1${separatorChar}`); return fixer.replaceText(node, result); } }); } } function getStyle(node) { if (!stencil.isComponent() || !options || !Object.keys(options).length) { return; } const decorators = getDecorator(node); decorators.filter((dec) => stencilDecorators.includes(decoratorName(dec))).forEach(checkStyle); } return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty': getStyle, 'MethodDefinition[kind=method]': getStyle }); } }; const rule$5 = { meta: { docs: { description: 'This rule catches Stencil Element type not matching tag name.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); function parseTag(tag) { let result = tag[0].toUpperCase() + tag.slice(1); const tagBody = tag.split('-'); if (tagBody.length > 1) { result = tagBody.map((tpart) => tpart[0].toUpperCase() + tpart.slice(1)).join(''); } return result; } return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty > Decorator[expression.callee.name=Element]': (node) => { if (stencil.isComponent()) { const tagType = getType(node.parent); const component = getDecorator(node.parent.parent.parent, 'Component'); const [opts] = parseDecorator(component); if (!opts || !opts.tag) { return; } const parsedTag = `HTML${parseTag(opts.tag)}Element`; if (tagType !== parsedTag) { context.report({ node: node.parent.typeAnnotation, message: `@Element type is not matching tag for component (${parsedTag})`, fix(fixer) { return fixer.replaceText(node.parent.typeAnnotation.typeAnnotation, parsedTag); } }); } } } }); } }; /** * @fileoverview ESLint rules specific to Stencil JS projects. * @author Tom Chinery <tom.chinery@addtoevent.co.uk> */ const rule$6 = { meta: { docs: { description: 'This rule catches usage of hostData method.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- const stencil = stencilComponentContext(); return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition[key.name=hostData]': (node) => { if (stencil.isComponent()) { context.report({ node: node.key, message: `hostData() is deprecated and <Host> should be used in the render function instead.` }); } } }); } }; const DEFAULTS$2 = 'call-order'; const rule$7 = { meta: { docs: { description: 'This rule catches not consistently lifecycle methods order.', category: 'Possible Errors', recommended: true }, schema: [ { type: 'string', enum: ['alphabetical', 'call-order'] } ], type: 'layout' }, create(context) { const stencil = stencilComponentContext(); const opts = context.options[0] || {}; const option = opts || DEFAULTS$2; let order = [...stencilLifecycle]; if (option === 'alphabetical') { order.sort(); } let positionIndex = 0; const methodList = new Map(); function filterMethods(node) { if (!stencil.isComponent()) { return; } const methodName = node.key.name; if (stencilLifecycle.includes(methodName)) { methodList.set(methodName, node); } } function checkOrder(node) { if (!stencil.isComponent()) { return; } order = order.filter((method) => methodList.has(method)); methodList.forEach((methodNode, methodName) => { if (methodName !== order[positionIndex]) { context.report({ node: methodNode, message: `Stencil lifecycle method "${methodName}" is not in the right place` }); } positionIndex++; }); } return { 'ClassDeclaration': stencil.rules.ClassDeclaration, 'MethodDefinition': filterMethods, 'ClassDeclaration:exit': (node) => { stencil.rules['ClassDeclaration:exit'](node); checkOrder(); } }; } }; const rule$8 = { meta: { docs: { description: 'This rule catches Stencil Methods marked as private or protected.', category: 'Possible Errors', recommended: true }, schema: [], fixable: 'code', type: 'problem' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition[kind=method]': (node) => { if (stencil.isComponent() && getDecorator(node, 'Method')) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); if (isPrivate(originalNode)) { const text = String(originalNode.getFullText()); context.report({ node: node, message: `Class methods decorated with @Method() cannot be private nor protected`, fix(fixer) { return fixer.replaceText(node, text.replace(/(private |protected )/, '')); } }); } } } }); } }; const varsList = new Set(); const rule$9 = { meta: { docs: { description: 'This rule catches Stencil Watch for not defined variables in Prop or State.', category: 'Possible Errors', recommended: true }, schema: [], type: 'suggestion' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; function getVars(node) { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const varName = originalNode.parent.name.escapedText; varsList.add(varName); } function checkWatch(node) { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const varName = originalNode.expression.arguments[0].text; if (!varsList.has(varName)) { context.report({ node: node, message: `Watch decorator @Watch("${varName}") is not matching with any @Prop() or @State()` }); } } return { 'ClassDeclaration': stencil.rules.ClassDeclaration, 'ClassProperty > Decorator[expression.callee.name=Prop]': getVars, 'ClassProperty > Decorator[expression.callee.name=State]': getVars, 'MethodDefinition[kind=method] > Decorator[expression.callee.name=Watch]': checkWatch, 'ClassDeclaration:exit': (node) => { if (!stencil.isComponent()) { return; } stencil.rules['ClassDeclaration:exit'](node); varsList.clear(); } }; } }; const rule$a = { meta: { docs: { description: 'This rule catches own class methods marked as public.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition[kind=method]': (node) => { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const stencilDecorator = originalNode.decorators && originalNode.decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText)); const stencilCycle = stencilLifecycle.includes(originalNode.name && originalNode.name.escapedText); if (!stencilDecorator && !stencilCycle && !isPrivate(originalNode)) { const text = String(originalNode.getFullText()); context.report({ node: node, message: `Own class methods cannot be public`, fix(fixer) { const methodName = node.key.name; const result = text.replace('public ', '').replace(methodName, `private ${methodName}`); return fixer.replaceText(node, result); } }); } } }); } }; const rule$b = { meta: { docs: { description: 'This rule catches own class attributes marked as public.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty': (node) => { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const stencilDecorator = originalNode.decorators && originalNode.decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText)); if (!stencilDecorator && !isPrivate(originalNode)) { const text = String(originalNode.getFullText()); context.report({ node: node, message: `Own class properties cannot be public`, fix(fixer) { const varName = node.key.name; const result = text.replace('public ', '').replace(varName, `private ${varName}`); return fixer.replaceText(node, result); } }); } } }); } }; const rule$c = { meta: { docs: { description: 'This rule catches usages of events using @Listen decorator.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { const stencil = stencilComponentContext(); return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition[kind=method]': (node) => { if (!stencil.isComponent()) { return; } const listenDec = getDecorator(node, 'Listen'); if (listenDec) { const [eventName, opts] = parseDecorator(listenDec); if (typeof eventName === 'string' && opts === undefined) { const eventName = listenDec.expression.arguments[0].value; if (PREFER_VDOM_LISTENER.includes(eventName)) { context.report({ node: listenDec, message: `Use vDOM listener instead.` }); } } } } }); } }; const PREFER_VDOM_LISTENER = [ 'click', 'touchstart', 'touchend', 'touchmove', 'mousedown', 'mouseup', 'mousemove', 'keyup', 'keydown', 'focusin', 'focusout', 'focus', 'blur' ]; const rule$d = { meta: { docs: { description: 'This rule catches Stencil Props marked as private or protected.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty': (node) => { if (stencil.isComponent() && getDecorator(node, 'Prop')) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); if (isPrivate(originalNode)) { const text = String(originalNode.getFullText()); context.report({ node: node, message: `Class properties decorated with @Prop() cannot be private nor protected`, fix(fixer) { return fixer.replaceText(node, text.replace(/(private |protected )/, '')); } }); } } } }); } }; const rule$e = { meta: { docs: { description: 'This rule catches Stencil Props marked as non readonly.', category: 'Possible Errors', recommended: true }, schema: [], type: 'layout', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty': (node) => { const propDecorator = getDecorator(node, 'Prop'); if (stencil.isComponent() && propDecorator) { const [opts] = parseDecorator(propDecorator); if (opts && opts.mutable === true) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const hasReadonly = !!(originalNode.modifiers && originalNode.modifiers.some(m => m.kind === ts__default.SyntaxKind.ReadonlyKeyword)); if (!hasReadonly) { context.report({ node: node.key, message: `Class properties decorated with @Prop() should be readonly`, fix(fixer) { return fixer.insertTextBefore(node.key, 'readonly '); } }); } } } }); } }; /** * @fileoverview ESLint rules specific to Stencil JS projects. * @author Tom Chinery <tom.chinery@addtoevent.co.uk> */ const rule$f = { meta: { docs: { description: 'This rule catches Stencil Prop names that share names of Global HTML Attributes.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- const stencil = stencilComponentContext(); const parserServices = context.parserServices; const typeChecker = parserServices.program.getTypeChecker(); return Object.assign(Object.assign({}, stencil.rules), { 'MethodDefinition[kind=method][key.name=render] ReturnStatement': (node) => { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node.argument); const type = typeChecker.getTypeAtLocation(originalNode); if (type && type.symbol && type.symbol.escapedName === 'Array') { context.report({ node: node, message: `Avoid returning an array in the render() function, use <Host> instead.` }); } } }); } }; const DECORATORS = ['Prop', 'Method', 'Event']; const INVALID_TAGS = ['type', 'memberof']; const rule$g = { meta: { docs: { description: 'This rule catches Stencil Props and Methods using jsdoc.', category: 'Possible Errors', recommended: true }, schema: [], type: 'layout' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.parserServices; function getJSDoc(node) { if (!stencil.isComponent()) { return; } DECORATORS.forEach((decName) => { if (getDecorator(node, decName)) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const jsDoc = originalNode.jsDoc; const isValid = jsDoc && jsDoc.length; const haveTags = isValid && jsDoc.some((jsdoc) => jsdoc.tags && jsdoc.tags.length && jsdoc.tags.some((tag) => INVALID_TAGS.includes(tag.tagName.escapedText.toLowerCase()))); if (!isValid) { context.report({ node: node, message: `The @${decName} decorator must to be documented.` }); } else if (haveTags) { context.report({ node: node, message: `The @${decName} decorator have not valid tags (${INVALID_TAGS.join(', ')}).` }); } } }); } return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty': getJSDoc, 'MethodDefinition[kind=method]': getJSDoc }); } }; const rule$h = { meta: { docs: { description: 'This rule catches required prefix in component tag name.', category: 'Possible Errors', recommended: false }, schema: [ { type: 'array', minLength: 1, additionalProperties: false } ], type: 'layout' }, create(context) { const stencil = stencilComponentContext(); return Object.assign(Object.assign({}, stencil.rules), { 'ClassDeclaration': (node) => { const component = getDecorator(node, 'Component'); if (!component) { return; } const [{ tag }] = parseDecorator(component); const options = context.options[0]; const match = options.some((t) => tag.startsWith(t)); if (!match) { context.report({ node: node, message: `The component with tagName ${tag} have not a valid prefix.` }); } } }); } }; /** * @fileoverview ESLint rules specific to Stencil JS projects. * @author Tom Chinery <tom.chinery@addtoevent.co.uk> */ const rule$i = { meta: { docs: { description: 'This rule catches Stencil Prop names that share names of Global HTML Attributes.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- const stencil = stencilComponentContext(); const checkName = (node) => { if (!stencil.isComponent()) { return; } const decoratorName = node.expression.callee.name; if (decoratorName === 'Prop' || decoratorName === 'Method') { const propName = node.parent.key.name; if (isReservedMember(propName)) { context.report({ node: node.parent.key, message: `The @${decoratorName} name "${propName} conflicts with a key in the HTMLElement prototype. Please choose a different name.` }); } if (propName.startsWith('data-')) { context.report({ node: node.parent.key, message: 'Avoid using Global HTML Attributes as Prop names.' }); } } }; return Object.assign(Object.assign({}, stencil.rules), { 'ClassProperty > Decorator[expression.callee.name=Prop]': checkName, 'MethodDefinition[kind=method] > Decorator[expression.callee.name=Method]': checkName }); } }; const HTML_ELEMENT_KEYS = [ 'title', 'lang', 'translate', 'dir', // 'dataset', // 'hidden', 'tabIndex', 'accessKey', 'draggable', // 'spellcheck', // 'autocapitalize', 'contentEditable', 'isContentEditable', // 'inputMode', 'offsetParent', 'offsetTop', 'offsetLeft', 'offsetWidth', 'offsetHeight', 'style', 'innerText', 'outerText', 'oncopy', 'oncut', 'onpaste', 'onabort', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll', 'onseeked', 'onseeking', 'onselect', 'onstalled', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting', 'onwheel', 'onauxclick', 'ongotpointercapture', 'onlostpointercapture', 'onpointerdown', 'onpointermove', 'onpointerup', 'onpointercancel', 'onpointerover', 'onpointerout', 'onpointerenter', 'onpointerleave', 'onselectstart', 'onselectionchange', 'nonce', 'click', 'focus', 'blur' ]; const ELEMENT_KEYS = [ 'namespaceURI', 'prefix', 'localName', 'tagName', 'id', 'className', 'classList', 'slot', 'attributes', 'shadowRoot', 'assignedSlot', 'innerHTML', 'outerHTML', 'scrollTop', 'scrollLeft', 'scrollWidth', 'scrollHeight', 'clientTop', 'clientLeft', 'clientWidth', 'clientHeight', 'attributeStyleMap', 'onbeforecopy', 'onbeforecut', 'onbeforepaste', 'onsearch', 'previousElementSibling', 'nextElementSibling', 'children', 'firstElementChild', 'lastElementChild', 'childElementCount', 'onfullscreenchange', 'onfullscreenerror', 'onwebkitfullscreenchange', 'onwebkitfullscreenerror', 'setPointerCapture', 'releasePointerCapture', 'hasPointerCapture', 'hasAttributes', 'getAttributeNames', 'getAttribute', 'getAttributeNS', 'setAttribute', 'setAttributeNS', 'removeAttribute', 'removeAttributeNS', 'hasAttribute', 'hasAttributeNS', 'toggleAttribute', 'getAttributeNode', 'getAttributeNodeNS', 'setAttributeNode', 'setAttributeNodeNS', 'removeAttributeNode', 'closest', 'matches', 'webkitMatchesSelector', 'attachShadow', 'getElementsByTagName', 'getElementsByTagNameNS', 'getElementsByClassName', 'insertAdjacentElement', 'insertAdjacentText', 'insertAdjacentHTML', 'requestPointerLock', 'getClientRects', 'getBoundingClientRect', 'scrollIntoView', 'scroll', 'scrollTo', 'scrollBy', 'scrollIntoViewIfNeeded', 'animate', 'computedStyleMap', 'before', 'after', 'replaceWith', 'remove', 'prepend', 'append', 'querySelector', 'querySelectorAll', 'requestFullscreen', 'webkitRequestFullScreen', 'webkitRequestFullscreen', 'part', 'createShadowRoot', 'getDestinationInsertionPoints' ]; const NODE_KEYS = [ 'ELEMENT_NODE', 'ATTRIBUTE_NODE', 'TEXT_NODE', 'CDATA_SECTION_NODE', 'ENTITY_REFERENCE_NODE', 'ENTITY_NODE', 'PROCESSING_INSTRUCTION_NODE', 'COMMENT_NODE', 'DOCUMENT_NODE', 'DOCUMENT_TYPE_NODE', 'DOCUMENT_FRAGMENT_NODE', 'NOTATION_NODE', 'DOCUMENT_POSITION_DISCONNECTED', 'DOCUMENT_POSITION_PRECEDING', 'DOCUMENT_POSITION_FOLLOWING', 'DOCUMENT_POSITION_CONTAINS', 'DOCUMENT_POSITION_CONTAINED_BY', 'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC', 'nodeType', 'nodeName', 'baseURI', 'isConnected', 'ownerDocument', 'parentNode', 'parentElement', 'childNodes', 'firstChild', 'lastChild', 'previousSibling', 'nextSibling', 'nodeValue', 'textContent', 'hasChildNodes', 'getRootNode', 'normalize', 'cloneNode', 'isEqualNode', 'isSameNode', 'compareDocumentPosition', 'contains', 'lookupPrefix', 'lookupNamespaceURI', 'isDefaultNamespace', 'insertBefore', 'appendChild', 'replaceChild', 'removeChild' ]; const JSX_KEYS = [ 'ref', 'key' ]; const RESERVED_PUBLIC_MEMBERS = new Set([ ...HTML_ELEMENT_KEYS, ...ELEMENT_KEYS, ...NODE_KEYS, ...JSX_KEYS ].map(p => p.toLowerCase())); function isReservedMember(memberName) { memberName = memberName.toLowerCase(); return RESERVED_PUBLIC_MEMBERS.has(memberName); } const rule$j = { meta: { docs: { description: 'This rule catches modules that expose more than just the Stencil Component itself.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { const parserServices = context.parserServices; const typeChecker = parserServices.program.getTypeChecker(); return { 'ClassDeclaration': (node) => { const component = getDecorator(node, 'Component'); if (component) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const nonTypeExports = typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(originalNode.getSourceFile())) .filter(symbol => (symbol.flags & (ts__default.SymbolFlags.Interface | ts__default.SymbolFlags.TypeAlias)) === 0) .filter(symbol => symbol.name !== originalNode.name.text); nonTypeExports.forEach(symbol => { const errorNode = (symbol.valueDeclaration) ? parserServices.tsNodeToESTreeNodeMap.get(symbol.valueDeclaration).id : parserServices.tsNodeToESTreeNodeMap.get(symbol.declarations[0]); context.report({ node: errorNode, message: `To allow efficient bundling, modules using @Component() can only have a single export which is the component class itself. Any other exports should be moved to a separate file. For further information check out: https://stenciljs.com/docs/module-bundling` }); }); } } }; } }; const mutableProps = new Map(); const rule$k = { meta: { docs: { description: 'This rule catches mutable Props that not need to be mutable.', category: 'Possible Errors', recommended: true }, schema: [], type: 'layout', fixable: 'code' }, create(context) { const stencil = stencilComponentContext(); function getMutable(node) { if (!stencil.isComponent()) { return; } const parsed = parseDecorator(node); const mutable = parsed && parsed.length && parsed[0].mutable || false; if (mutable) { const varName = node.parent.key.name; mutableProps.set(varName, node); } } function checkAssigment(node) { if (!stencil.isComponent()) { return; } const propName = node.left.property.name; mutableProps.delete(propName); } stencil.rules['ClassDeclaration:exit']; return { 'ClassDeclaration': stencil.rules.ClassDeclaration, 'ClassProperty > Decorator[expression.callee.name=Prop]': getMutable, 'AssignmentExpression[left.object.type=ThisExpression][left.property.type=Identifier]': checkAssigment, 'ClassDeclaration:exit': (node) => { const isCmp = stencil.isComponent(); stencil.rules['ClassDeclaration:exit'](node); if (isCmp) { mutableProps.forEach((varNode, varName) => { context.report({ node: varNode.parent, message: `@Prop() "${varName}" should not be mutable`, }); }); mutableProps.clear(); } } }; } }; const rule$l = { meta: { docs: { description: 'This rule catches function calls at the top level', category: 'Possible Errors', recommended: false }, schema: [ { type: 'array', items: { type: 'string' }, minLength: 0, additionalProperties: false } ], type: 'suggestion' }, create(context) { const shouldSkip = /\b(spec|e2e|test)\./.test(context.getFilename()); const skipFunctions = context.options[0] || DEFAULTS$3; if (shouldSkip) { return {}; } return { 'CallExpression': (node) => { if (skipFunctions.includes(node.callee.name)) { return; } if (!isInScope(node)) { context.report({ node: node, message: `Call expressions at the top-level should be avoided.` }); } } }; } }; const isInScope = (n) => { const type = n.type; if (type === 'ArrowFunctionExpression' || type === 'FunctionDeclaration' || type === 'ClassDeclaration' || type === 'ExportNamedDeclaration') { return true; } n = n.parent; if (n) { return isInScope(n); } return false; }; const DEFAULTS$3 = ['describe', 'test', 'bind', 'createStore']; /** * @license * Copyright 2016 Palantir Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const OPTION_ALLOW_NULL_UNION = 'allow-null-union'; const OPTION_ALLOW_UNDEFINED_UNION = 'allow-undefined-union'; const OPTION_ALLOW_STRING = 'allow-string'; const OPTION_ALLOW_ENUM = 'allow-enum'; const OPTION_ALLOW_NUMBER = 'allow-number'; const OPTION_ALLOW_MIX = 'allow-mix'; const OPTION_ALLOW_BOOLEAN_OR_UNDEFINED = 'allow-boolean-or-undefined'; const OPTION_ALLOW_ANY_RHS = 'allow-any-rhs'; const rule$m = { meta: { docs: { description: `Restricts the types allowed in boolean expressions. By default only booleans are allowed. The f