UNPKG

@stencil-community/eslint-plugin

Version:
1,383 lines (1,356 loc) 70.2 kB
'use strict'; var react = require('eslint-plugin-react'); var ts = require('typescript'); var eslintUtils = require('eslint-utils'); var tsutils = require('tsutils'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var ts__namespace = /*#__PURE__*/_interopNamespaceDefault(ts); const SyntaxKind = ts.SyntaxKind; function isPrivate(originalNode) { const modifiers = ts.canHaveModifiers(originalNode) ? ts.getModifiers(originalNode) : undefined; if (modifiers) { return modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword); } // detect private identifier (#) const firstChildNode = originalNode.getChildAt(0); return firstChildNode ? firstChildNode.kind === SyntaxKind.PrivateIdentifier : false; } function getDecoratorList(originalNode) { const decorators = ts.canHaveDecorators(originalNode) ? ts.getDecorators(originalNode) : undefined; return decorators; } 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.expression && 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) { 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', 'AttachInternals']; const stencilLifecycle = [ 'connectedCallback', 'disconnectedCallback', 'componentWillLoad', 'componentDidLoad', 'componentWillRender', 'componentDidRender', 'componentShouldUpdate', 'componentWillUpdate', 'componentDidUpdate', 'formAssociatedCallback', 'formDisabledCallback', 'formResetCallback', 'formStateRestoreCallback', 'render' ]; ({ [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$o = { 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.sourceCode.parserServices; const typeChecker = parserServices.program.getTypeChecker(); return { ...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 // a newline + whitespace preceding `@Method` may be captured, remove it .trimLeft() // capture the number of following the decorator to know how far to indent the `async` method .replace(/@Method\(\)\n(\s*)/, '@Method()\n$1async ') // replace any inlined @Method decorators .replace('@Method() ', '@Method() async') // swap the order of the `async` and `public` keywords .replace('async public', 'public async') // swap the order of the `async` and `private` keywords .replace('async private', 'private async'); return fixer.replaceText(node, result); } }); } } }; } }; const rule$n = { meta: { docs: { description: 'This rule catches Stencil Props defaulting to true.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', }, create(context) { const stencil = stencilComponentContext(); return { ...stencil.rules, 'PropertyDefinition': (node) => { const propDecorator = getDecorator(node, 'Prop'); if (!(stencil.isComponent() && propDecorator)) { return; } if (node.value?.value === true) { context.report({ node: node, message: `Boolean properties decorated with @Prop() should default to false` }); } } }; } }; const DEFAULTS$2 = ['stencil', 'stnl', 'st']; const rule$m = { 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 { ...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$2; 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$l = { 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.sourceCode.parserServices; return { ...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$k = { 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 { ...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 !== 'PropertyDefinition' && (node.parent.type === 'MethodDefinition' && ['get', 'set'].indexOf(node.parent.kind) < 0)) { 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$j = { 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 } } } ], type: 'layout' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.sourceCode.parserServices; const opts = context.options[0] || {}; const options = { ...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}.`, }); } } 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 { ...stencil.rules, 'PropertyDefinition': getStyle, 'MethodDefinition[kind=method]': getStyle }; } }; const rule$i = { 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 { ...stencil.rules, 'PropertyDefinition > 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 ?? node.parent, message: `@Element type is not matching tag for component (${parsedTag})`, fix(fixer) { // If the property has a type annotation, we can replace just that node with the parsed tag // @Element() elm: HTMLElement; -> @Element() elm: HTMLSampleTagElement; if (node.parent.typeAnnotation?.typeAnnotation) { return fixer.replaceText(node.parent.typeAnnotation.typeAnnotation, parsedTag); } // If no type annotation exists on the property, we'll do some string manipulation to insert one. // @Element() elm; -> @Element() elm: HTMLSampleTagElement; const text = context.sourceCode.getText(node.parent).replace(';', '').concat(`: ${parsedTag};`); return fixer.replaceText(node.parent, text); } }); } } } }; } }; /** * @fileoverview ESLint rules specific to Stencil JS projects. * @author Tom Chinery <tom.chinery@addtoevent.co.uk> */ const rule$h = { 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 { ...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 rule$g = { meta: { docs: { description: 'This rule catches Stencil Methods marked as private or protected.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem' }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.sourceCode.parserServices; return { ...stencil.rules, 'MethodDefinition[kind=method]': (node) => { if (stencil.isComponent() && getDecorator(node, 'Method')) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); if (isPrivate(originalNode)) { context.report({ node: node, message: `Class methods decorated with @Method() cannot be private nor protected` }); } } } }; } }; const varsList = new Set(); const rule$f = { 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.sourceCode.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) && !isReservedAttribute(varName.toLowerCase())) { context.report({ node: node, message: `Watch decorator @Watch("${varName}") is not matching with any @Prop() or @State()`, }); } } return { ClassDeclaration: stencil.rules.ClassDeclaration, 'PropertyDefinition > Decorator[expression.callee.name=Prop]': getVars, 'MethodDefinition[kind=get] > Decorator[expression.callee.name=Prop]': getVars, 'MethodDefinition[kind=set] > Decorator[expression.callee.name=Prop]': getVars, 'PropertyDefinition > 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(); } }; } }; // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes const GLOBAL_ATTRIBUTES$1 = [ 'about', 'accessKey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'contextmenu', 'dir', 'draggable', 'enterkeyhint', 'hidden', 'id', 'inert', 'inputmode', 'id', 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style', 'tabindex', 'title', 'translate', 'virtualkeyboardpolicy', ]; const RESERVED_PUBLIC_ATTRIBUTES = new Set([ ...GLOBAL_ATTRIBUTES$1, ].map(p => p.toLowerCase())); function isReservedAttribute(attributeName) { return RESERVED_PUBLIC_ATTRIBUTES.has(attributeName.toLowerCase()); } const rule$e = { 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.sourceCode.parserServices; return { ...stencil.rules, "MethodDefinition[kind=method]": (node) => { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const decorators = getDecoratorList(originalNode); const stencilDecorator = decorators && decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText)); const stencilCycle = stencilLifecycle.includes(originalNode.name.escapedText); if (!stencilDecorator && !stencilCycle && !isPrivate(originalNode)) { context.report({ node: node, message: `Own class methods cannot be public`, fix(fixer) { const sourceCode = context.getSourceCode(); const tokens = sourceCode.getTokens(node); const publicToken = tokens.find(token => token.value === 'public'); if (publicToken) { return fixer.replaceText(publicToken, 'private'); } else { return fixer.insertTextBefore(node.key, 'private '); } } }); } }, }; }, }; const rule$d = { 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.sourceCode.parserServices; return { ...stencil.rules, PropertyDefinition: (node) => { if (!stencil.isComponent()) { return; } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const decorators = getDecoratorList(originalNode); const stencilDecorator = decorators && decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText)); if (!stencilDecorator && !isPrivate(originalNode)) { context.report({ node: node, message: `Own class properties cannot be public`, fix(fixer) { const sourceCode = context.getSourceCode(); const tokens = sourceCode.getTokens(node); const publicToken = tokens.find(token => token.value === 'public'); if (publicToken) { return fixer.replaceText(publicToken, 'private'); } else { return fixer.insertTextBefore(node.key, 'private '); } } }); } }, }; }, }; 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 { ...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$b = { meta: { docs: { description: 'This rule catches Stencil Props marked as private or protected.', category: 'Possible Errors', recommended: true }, schema: [], type: 'problem', }, create(context) { const stencil = stencilComponentContext(); const parserServices = context.sourceCode.parserServices; return { ...stencil.rules, 'PropertyDefinition': (node) => { if (stencil.isComponent() && getDecorator(node, 'Prop')) { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); if (isPrivate(originalNode)) { context.report({ node: node, message: `Class properties decorated with @Prop() cannot be private nor protected` }); } } } }; } }; const rule$a = { 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.sourceCode.parserServices; return { ...stencil.rules, 'PropertyDefinition': (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 = !!(ts.canHaveModifiers(originalNode) && ts.getModifiers(originalNode)?.some(m => m.kind === ts.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$9 = { 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.sourceCode.parserServices; const typeChecker = parserServices.program.getTypeChecker(); return { ...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$8 = { 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.sourceCode.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 be documented.` }); } else if (haveTags) { context.report({ node: node, message: `The @${decName} decorator have not valid tags (${INVALID_TAGS.join(', ')}).` }); } } }); } return { ...stencil.rules, 'PropertyDefinition': getJSDoc, 'MethodDefinition[kind=method]': getJSDoc }; } }; const rule$7 = { 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 { ...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 jsdom = require("jsdom"); const { JSDOM } = jsdom; const rule$6 = { 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 { ...stencil.rules, 'PropertyDefinition > Decorator[expression.callee.name=Prop]': checkName, 'MethodDefinition[kind=method] > Decorator[expression.callee.name=Method]': checkName }; } }; // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes const GLOBAL_ATTRIBUTES = [ 'about', 'accessKey', 'autocapitalize', 'autofocus', 'class', 'contenteditable', 'contextmenu', 'dir', 'draggable', 'enterkeyhint', 'hidden', 'id', 'inert', 'inputmode', 'id', 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype', 'lang', 'nonce', 'part', 'popover', 'role', 'slot', 'spellcheck', 'style', 'tabindex', 'title', 'translate', 'virtualkeyboardpolicy', ]; const JSX_KEYS = [ 'ref', 'key' ]; function getHtmlElementProperties() { const { window: win } = new JSDOM(); const { document: doc } = win; const htmlElement = doc.createElement("tester-component"); // creates a custom element base (HTMLElement) const relevantInterfaces = [win.HTMLElement, win.Element, win.Node, win.EventTarget]; const props = new Set(); let currentInstance = htmlElement; while (currentInstance && relevantInterfaces.some(relevantInterface => currentInstance instanceof relevantInterface)) { Object.getOwnPropertyNames(currentInstance).forEach((prop) => props.add(prop)); currentInstance = Object.getPrototypeOf(currentInstance); } return Array.from(props); } const RESERVED_PUBLIC_MEMBERS = new Set([ ...GLOBAL_ATTRIBUTES, ...getHtmlElementProperties(), ...JSX_KEYS ].map(p => p.toLowerCase())); function isReservedMember(memberName) { return RESERVED_PUBLIC_MEMBERS.has(memberName.toLowerCase()); } const rule$5 = { 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.sourceCode.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.SymbolFlags.Interface | ts.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 mutableAssigned = new Set(); const rule$4 = { meta: { docs: { description: 'This rule catches mutable Props that not need to be mutable.', category: 'Possible Errors', recommended: true }, schema: [], type: 'layout', }, 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; mutableAssigned.add(propName); } return { 'ClassDeclaration': stencil.rules.ClassDeclaration, 'PropertyDefinition > 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) { mutableAssigned.forEach((propName) => mutableProps.delete(propName)); mutableProps.forEach((varNode, varName) => { context.report({ node: varNode.parent, message: `@Prop() "${varName}" should not be mutable`, }); }); mutableAssigned.clear(); mutableProps.clear(); } } }; } }; const rule$3 = { 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; 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 = ['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$2 = { meta: { docs: { description: `Restricts the types allowed in boolean expressions. By default only booleans are allowed. The following nodes are checked: * Arguments to the \`!\`, \`&&\`, and \`||\` operators * The condition in a conditional expression (\`cond ? x : y\`) * Conditions for \`if\`, \`for\`, \`while\`, and \`do-while\` statements.`, category: 'Possible Errors', recommended: true }, schema: [{ type: "array", items: { type: "string", enum: [ OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_ENUM, OPTION_ALLOW_NUMBER, OPTION_ALLOW_BOOLEAN_OR_UNDEFINED, OPTION_ALLOW_ANY_RHS ], }, minLength: 0, maxLength: 5, }], type: 'problem' }, create(context) { const parserServices = context.sourceCode.parserServices; const program = parserServices.program; const rawOptions = context.options[0] || ['allow-null-union', 'allow-undefined-union', 'allow-boolean-or-undefined']; const options = parseOptions(rawOptions, true); const checker = program.getTypeChecker(); function walk(sourceFile) { ts__namespace.forEachChild(sourceFile, function cb(node) { switch (node.kind) { case ts__namespace.SyntaxKind.Pref