UNPKG

@stencil/eslint-plugin

Version:

ESLint rules specific to Stencil JS projects.

1,590 lines (1,561 loc) 53.6 kB
import react from "eslint-plugin-react"; import * as ts$1 from "typescript"; import ts from "typescript"; import { getStaticValue } from "eslint-utils"; import { isThenableType } from "tsutils"; import { JSDOM } from "jsdom"; //#region src/utils.ts const SyntaxKind = ts.SyntaxKind; function isPrivate(originalNode) { const modifiers = ts.canHaveModifiers(originalNode) ? ts.getModifiers(originalNode) : void 0; if (modifiers) return modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword); const firstChildNode = originalNode.getChildAt(0); return firstChildNode ? firstChildNode.kind === SyntaxKind.PrivateIdentifier : false; } function getDecoratorList(originalNode) { const decorators = ts.canHaveDecorators(originalNode) ? ts.getDecorators(originalNode) : void 0; return decorators; } function getDecorator(node, decoratorName$1) { if (decoratorName$1) return node.decorators && node.decorators.find(isDecoratorNamed(decoratorName$1)); 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 = getStaticValue(a); return parsed ? parsed.value : void 0; }); 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 = void 0; } }, 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" ]; 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" }; //#endregion //#region src/rules/async-methods.ts const rule$24 = { 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 (!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.trimLeft().replace(/@Method\(\)\n(\s*)/, "@Method()\n$1async ").replace("@Method() ", "@Method() async").replace("async public", "public async").replace("async private", "private async"); return fixer.replaceText(node, result); } }); } } }; } }; var async_methods_default = rule$24; //#endregion //#region src/rules/ban-default-true.ts const rule$23 = { 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, message: `Boolean properties decorated with @Prop() should default to false` }); } }; } }; var ban_default_true_default = rule$23; //#endregion //#region src/rules/ban-prefix.ts const DEFAULTS$2 = [ "stencil", "stnl", "st" ]; const rule$22 = { 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, message: `The component with tag name ${tag} have a banned prefix.` }); } }; } }; var ban_prefix_default = rule$22; //#endregion //#region src/rules/class-pattern.ts const rule$21 = { 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" : void 0); if (!regExp.test(className)) { const [opts] = parseDecorator(component); if (!opts || !opts.tag) return; context.report({ node, message: `The class name in component with tag name ${opts.tag} is not valid (${regExp}).` }); } } }; } }; var class_pattern_default = rule$21; //#endregion //#region src/rules/decorators-context.ts const rule$20 = { 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, 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, message: `The @${decName} decorator can only be applied to class methods.` }); } else if (decName === "Component") { if (node.parent.type !== "ClassDeclaration") context.report({ node, message: `The @${decName} decorator can only be applied to a class.` }); } } } }; } }; var decorators_context_default = rule$20; //#endregion //#region src/rules/decorators-style.ts const ENUMERATE = [ "inline", "multiline", "ignore" ]; const DEFAULTS$1 = { prop: "ignore", state: "ignore", element: "ignore", event: "ignore", method: "ignore", watch: "ignore", listen: "ignore" }; const rule$19 = { 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, 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 }; } }; var decorators_style_default = rule$19; //#endregion //#region src/rules/element-type.ts const rule$18 = { 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 (node.parent.typeAnnotation?.typeAnnotation) return fixer.replaceText(node.parent.typeAnnotation.typeAnnotation, parsedTag); const text = context.sourceCode.getText(node.parent).replace(";", "").concat(`: ${parsedTag};`); return fixer.replaceText(node.parent, text); } }); } } }; } }; var element_type_default = rule$18; //#endregion //#region src/rules/host-data-deprecated.ts const rule$17 = { meta: { docs: { description: "This rule catches usage of hostData method.", category: "Possible Errors", recommended: true }, schema: [], type: "problem" }, create(context) { 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.` }); } }; } }; var host_data_deprecated_default = rule$17; //#endregion //#region src/rules/methods-must-be-public.ts const rule$16 = { 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, message: `Class methods decorated with @Method() cannot be private nor protected` }); } } }; } }; var methods_must_be_public_default = rule$16; //#endregion //#region src/rules/no-unused-watch.ts const varsList = /* @__PURE__ */ new Set(); const rule$15 = { 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, 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(); } }; } }; 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()); } var no_unused_watch_default = rule$15; //#endregion //#region src/rules/own-methods-must-be-private.ts const rule$14 = { 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, 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 "); } }); } }; } }; var own_methods_must_be_private_default = rule$14; //#endregion //#region src/rules/own-props-must-be-private.ts const rule$13 = { 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, 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 "); } }); } }; } }; var own_props_must_be_private_default = rule$13; //#endregion //#region src/rules/prefer-vdom-listener.ts const rule$12 = { 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 === void 0) { const eventName$1 = listenDec.expression.arguments[0].value; if (PREFER_VDOM_LISTENER.includes(eventName$1)) 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" ]; var prefer_vdom_listener_default = rule$12; //#endregion //#region src/rules/props-must-be-public.ts const rule$11 = { 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, message: `Class properties decorated with @Prop() cannot be private nor protected` }); } } }; } }; var props_must_be_public_default = rule$11; //#endregion //#region src/rules/props-must-be-readonly.ts const rule$10 = { 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 "); } }); } } }; } }; var props_must_be_readonly_default = rule$10; //#endregion //#region src/rules/render-returns-host.ts 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) { 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, message: `Avoid returning an array in the render() function, use <Host> instead.` }); } }; } }; var render_returns_host_default = rule$9; //#endregion //#region src/rules/required-jsdoc.ts 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, message: `The @${decName} decorator must be documented.` }); else if (haveTags) context.report({ node, message: `The @${decName} decorator have not valid tags (${INVALID_TAGS.join(", ")}).` }); } }); } return { ...stencil.rules, "PropertyDefinition": getJSDoc, "MethodDefinition[kind=method]": getJSDoc }; } }; var required_jsdoc_default = rule$8; //#endregion //#region src/rules/required-prefix.ts 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, message: `The component with tagName ${tag} have not a valid prefix.` }); } }; } }; var required_prefix_default = rule$7; //#endregion //#region src/rules/reserved-member-names.ts 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) { const stencil = stencilComponentContext(); const checkName = (node) => { if (!stencil.isComponent()) return; const decoratorName$1 = node.expression.callee.name; if (decoratorName$1 === "Prop" || decoratorName$1 === "Method") { const propName = node.parent.key.name; if (isReservedMember(propName)) context.report({ node: node.parent.key, message: `The @${decoratorName$1} 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 }; } }; 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"); const relevantInterfaces = [ win.HTMLElement, win.Element, win.Node, win.EventTarget ]; const props = /* @__PURE__ */ 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()); } var reserved_member_names_default = rule$6; //#endregion //#region src/rules/single-export.ts 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` }); }); } } }; } }; var single_export_default = rule$5; //#endregion //#region src/rules/strict-mutable.ts const mutableProps = /* @__PURE__ */ new Map(); const mutableAssigned = /* @__PURE__ */ 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); } stencil.rules["ClassDeclaration:exit"]; 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(); } } }; } }; var strict_mutable_default = rule$4; //#endregion //#region src/rules/ban-side-effects.ts 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, 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" ]; var ban_side_effects_default = rule$3; //#endregion //#region src/rules/strict-boolean-conditions.ts 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$1.forEachChild(sourceFile, function cb(node) { switch (node.kind) { case ts$1.SyntaxKind.PrefixUnaryExpression: { const { operator, operand } = node; if (operator === ts$1.SyntaxKind.ExclamationToken) checkExpression(operand, node); break; } case ts$1.SyntaxKind.IfStatement: case ts$1.SyntaxKind.WhileStatement: case ts$1.SyntaxKind.DoStatement: { const c = node; checkExpression(c.expression, c); break; } case ts$1.SyntaxKind.ConditionalExpression: checkExpression(node.condition, node); break; case ts$1.SyntaxKind.ForStatement: { const { condition } = node; if (condition !== void 0) checkExpression(condition, node); } } return ts$1.forEachChild(node, cb); }); function checkExpression(node, location) { const type = checker.getTypeAtLocation(node); const failure = getTypeFailure(type, options); if (failure !== void 0) { if (failure === TypeFailure.AlwaysTruthy && !options.strictNullChecks && (options.allowNullUnion || options.allowUndefinedUnion)) return; const originalNode = parserServices.tsNodeToESTreeNodeMap.get(node); context.report({ node: originalNode, message: showFailure(location, failure, isUnionType(type), options) }); } } } return { "Program": (node) => { const sourceFile = parserServices.esTreeNodeToTSNodeMap.get(node); walk(sourceFile); } }; } }; function parseOptions(ruleArguments, strictNullChecks) { return { strictNullChecks, allowNullUnion: has(OPTION_ALLOW_NULL_UNION), allowUndefinedUnion: has(OPTION_ALLOW_UNDEFINED_UNION), allowString: has(OPTION_ALLOW_STRING), allowEnum: has(OPTION_ALLOW_ENUM), allowNumber: has(OPTION_ALLOW_NUMBER), allowMix: has(OPTION_ALLOW_MIX), allowBooleanOrUndefined: has(OPTION_ALLOW_BOOLEAN_OR_UNDEFINED), allowAnyRhs: has(OPTION_ALLOW_ANY_RHS) }; function has(name) { return ruleArguments.indexOf(name) !== -1; } } function getTypeFailure(type, options) { if (isUnionType(type)) return handleUnion(type, options); const kind = getKind(type); const failure = failureForKind(kind, false, options); if (failure !== void 0) return failure; switch (triState(kind)) { case true: return isTypeFlagSet(type, ts$1.TypeFlags.Any | ts$1.TypeFlags.BooleanLiteral) ? void 0 : TypeFailure.AlwaysTruthy; case false: return isTypeFlagSet(type, ts$1.TypeFlags.BooleanLiteral) ? void 0 : TypeFailure.AlwaysFalsy; case void 0: return void 0; } } function isBooleanUndefined(type) { let isTruthy = false; for (const ty of type.types) if (isTypeFlagSet(ty, ts$1.TypeFlags.Boolean)) isTruthy = true; else if (isTypeFlagSet(ty, ts$1.TypeFlags.BooleanLiteral)) isTruthy = isTruthy || ty.intrinsicName === "true"; else if (!isTypeFlagSet(ty, ts$1.TypeFlags.Void | ts$1.TypeFlags.Undefined)) return void 0; return isTruthy; } function handleUnion(type, options) { if (options.allowBooleanOrUndefined) switch (isBooleanUndefined(type)) { case true: return void 0; case false: return TypeFailure.AlwaysFalsy; } for (const ty of type.types) { const kind = getKind(ty); const failure = failureForKind(kind, true, options); if (failure !== void 0) return failure; } return void 0; } /** Fails if a kind of falsiness is not allowed. */ function failureForKind(kind, isInUnion, options) { switch (kind) { case TypeKind.String: case TypeKind.FalseStringLiteral: return options.allowString ? void 0 : TypeFailure.String; case TypeKind.Number: case TypeKind.FalseNumberLiteral: return options.allowNumber ? void 0 : TypeFailure.Number; case TypeKind.Enum: return options.allowEnum ? void 0 : TypeFailure.Enum; case TypeKind.Promise: return TypeFailure.Promise; case TypeKind.Null: return isInUnion && !options.allowNullUnion ? TypeFailure.Null : void 0; case TypeKind.Undefined: return isInUnion && !options.allowUndefinedUnion ? TypeFailure.Undefined : void 0; default: return void 0; } } let TypeFailure = /* @__PURE__ */ function(TypeFailure$1) { TypeFailure$1[TypeFailure$1["AlwaysTruthy"] = 0] = "AlwaysTruthy"; TypeFailure$1[TypeFailure$1["AlwaysFalsy"] = 1] = "AlwaysFalsy"; TypeFailure$1[TypeFailure$1["String"] = 2] = "String"; TypeFailure$1[TypeFailure$1["Number"] = 3] = "Number"; TypeFailure$1[TypeFailure$1["Null"] = 4] = "Null"; TypeFailure$1[TypeFailure$1["Undefined"] = 5] = "Undefined"; TypeFailure$1[TypeFailure$1["Enum"] = 6] = "Enum"; TypeFailure$1[TypeFailure$1["Mixes"] = 7] = "Mixes"; TypeFailure$1[TypeFailure$1["Promise"] = 8] = "Promise"; return TypeFailure$1; }({}); var TypeKind = /* @__PURE__ */ function(TypeKind$1) { TypeKind$1[TypeKind$1["String"] = 0] = "String"; TypeKind$1[TypeKind$1["FalseStringLiteral"] = 1] = "FalseStringLiteral"; TypeKind$1[TypeKind$1["Number"] = 2] = "Number"; TypeKind$1[TypeKind$1["FalseNumberLiteral"] = 3] = "FalseNumberLiteral"; TypeKind$1[TypeKind$1["Boolean"] = 4] = "Boolean"; TypeKind$1[TypeKind$1["FalseBooleanLiteral"] = 5] = "FalseBooleanLiteral"; TypeKind$1[TypeKind$1["Null"] = 6] = "Null"; TypeKind$1[TypeKind$1["Undefined"] = 7] = "Undefined"; TypeKind$1[TypeKind$1["Enum"] = 8] = "Enum"; TypeKind$1[TypeKind$1["AlwaysTruthy"] = 9] = "AlwaysTruthy"; TypeKind$1[TypeKind$1["Promise"] = 10] = "Promise"; return TypeKind$1; }(TypeKind || {}); /** Divides a type into always true, always false, or unknown. */ function triState(kind) { switch (kind) { case TypeKind.String: case TypeKind.Number: case TypeKind.Boolean: case TypeKind.Enum: return void 0; case TypeKind.Null: case TypeKind.Undefined: case TypeKind.FalseNumberLiteral: case TypeKind.FalseStringLiteral: case TypeKind.FalseBooleanLiteral: return false; case TypeKind.AlwaysTruthy: case TypeKind.Promise: return true; } } function getKind(type) { return is(ts$1.TypeFlags.StringLike) ? TypeKind.String : is(ts$1.TypeFlags.NumberLike) ? TypeKind.Number : is(ts$1.TypeFlags.Boolean) ? TypeKind.Boolean : isObject("Promise") ? TypeKind.Promise : is(ts$1.TypeFlags.Null) ? TypeKind.Null : is(ts$1.TypeFlags.Undefined | ts$1.TypeFlags.Void) ? TypeKind.Undefined : is(ts$1.TypeFlags.EnumLike) ? TypeKind.Enum : is(ts$1.TypeFlags.BooleanLiteral) ? type.intrinsicName === "true" ? TypeKind.AlwaysTruthy : TypeKind.FalseBooleanLiteral : TypeKind.AlwaysTruthy; function is(flags) { return isTypeFlagSet(type, flags); } function isObject(name) { const symbol = type.getSymbol(); return symbol && symbol.getName() === name; } } function binaryBooleanExpressionKind(node) { switch (node.operatorToken.kind) { case ts$1.SyntaxKind.AmpersandAmpersandToken: return "&&"; case ts$1.SyntaxKind.BarBarToken: return "||"; default: return void 0; } } function stringOr(parts) { switch (parts.length) { case 1: return parts[0]; case 2: return `${parts[0]} or ${parts[1]}`; default: let res = ""; for (let i = 0; i < parts.length - 1; i++) res += `${parts[i]}, `; return `${res}or ${parts[parts.length - 1]}`; } } function isUnionType(type) { return isTypeFlagSet(type, ts$1.TypeFlags.Union) && !isTypeFlagSet(type, ts$1.TypeFlags.Enum); } function showLocation(n) { switch (n.kind) { case ts$1.SyntaxKind.PrefixUnaryExpression: return "operand for the '!' operator"; case ts$1.SyntaxKind.ConditionalExpression: return "condition"; case ts$1.SyntaxKind.ForStatement: return "'for' condition"; case ts$1.SyntaxKind.IfStatement: return "'if' condition"; case ts$1.SyntaxKind.WhileStatement: return "'while' condition"; case ts$1.SyntaxKind.DoStatement: return "'do-while' condition"; case ts$1.SyntaxKind.BinaryExpression: return `operand for the '${binaryBooleanExpressionKind(n)}' operator`; } } function showFailure(location, ty, unionType, options) { const expectedTypes = showExpectedTypes(options); const expected = expectedTypes.length === 1 ? `Only ${expectedTypes[0]}s are allowed` : `Allowed types are ${stringOr(expectedTypes)}`; const tyFail = showTypeFailure(ty, unionType, options.strictNullChecks); return `This type is not allowed in the ${showLocation(location)} because it ${tyFail}. ${expected}.`; } function showExpectedTypes(options) { const parts = ["boolean"]; if (options.allowNullUnion) parts.push("null-union"); if (options.allowUndefinedUnion) parts.push("undefined-union"); if (options.allowString) parts.push("string"); if (options.allowEnum) parts.push("enum"); if (options.allowNumber) parts.push("number"); if (options.allowBooleanOrUndefined) parts.push("boolean-or-undefined"); return parts; } function showTypeFailure(ty, unionType, strictNullChecks) { const is = unionType ? "could be" : "is"; switch (ty) { case TypeFailure.AlwaysTruthy: return strictNullChecks ? "is always truthy" : `is always truthy. It may be null/undefined, but neither '${OPTION_ALLOW_NULL_UNION}' nor '${OPTION_ALLOW_UNDEFINED_UNION}' is set`; case TypeFailure.AlwaysFalsy: return "is always falsy"; case TypeFailure.String: return `${is} a string`; case TypeFailure.Number: return `${is} a number`; case TypeFailure.Null: return `${is} null`; case TypeFailure.Undefined: return `${is} undefined`; case TypeFailure.Enum: return `${is} an enum`; case TypeFailure.Promise: return "promise handled as boolean expression"; case TypeFailure.Mixes: return "unions more than one truthy/falsy type"; } } function isTypeFlagSet(obj, flag) { return (obj.flags & flag) !== 0; } var strict_boolean_conditions_default = rule$2; //#endregion //#region src/rules/ban-exported-const-enums.ts const rule$1 = { meta: { docs: { description: "This rule catches exports of const enums", category: "Possible Errors", recommended: true }, schema: [], type: "problem" }, create(context) { return { "ExportNamedDeclaration > TSEnumDeclaration[const]": (node) => { context.report({ node, message: `Exported const enums are not allowed` }); } }; } }; var ban_exported_const_enums_default = rule$1; //#endregion //#region src/rules/dependency-suggestions.ts const rule = { meta: { docs: { description: "This rule can provide suggestions about dependencies in stencil apps", recommended: true }, schema: [], type: "suggestion" }, create(context) { return { "ImportDeclaration": (node) => { const importName = node.source.value; const message = SUGGESTIONS[importName]; if (message) context.report({ node, message }); } }; } }; const SUGGESTIONS = { "classnames": `Stencil can already render conditional classes: <div class={{disabled: condition}}>`, "lodash": `"lodash" will bloat your build, use "lodash-es" instead: https://www.npmjs.com/package/lodash-es`, "moment": `"moment" will bloat your build, use "dayjs", "date-fns" or other modern lightweight alternaitve`, "core-js": `Stencil already include the core-js polyfills only when needed` }; var dependency_suggestions_default = rule; //#endregion //#region src/rules/index.ts var rules_default = { "ban-side-effects": ban_side_effects_default, "ban-default-true": ban_default_true_default, "ban-exported-const-enums": ban_exported_const_enums_default, "dependency-suggestions": dependency_suggestions_default, "strict-boolean-conditions": strict_boolean_conditions_default, "async-methods": async_methods_default, "ban-prefix": ban_prefix_default, "class-pattern": class_pattern_default, "decorators-context": decorators_context_default, "decorators-style": decorators_style_default, "element-type": element_type_default, "host-data-deprecated": host_data_deprecated_default, "methods-must-be-public": methods_must_be_public_default, "no-unused-watch": no_unused_watch_default, "own-methods-must-be-private": own_methods_must_be_private_default, "own-props-must-be-private": own_props_must_be_private_default, "prefer-vdom-listener": prefer_vdom_listener_default, "props-must-be-public": props_must_be_public_default, "props-must-be-readonly": props_must_be_readonly_default, "render-returns-host": render_returns_host_default, "required-jsdoc": required_jsdoc_default, "required-prefix": required_prefix_default, "reserved-member-names": reserved_member_names_default, "single-export": single_export_default, "strict-mutable": strict_mutable_default }; //#endregion //#region src/configs/base.ts var base_default = { overrides: [{ files: ["*.ts", "*.tsx"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: 2018, sourceType: "module", ecmaFeatures: { "jsx": true } }, env: { es2020: true, browser: true }, plugins: ["stencil"], rules: { "stencil/async-methods": 2, "stencil/ban-prefix": [2, [ "stencil", "stnl", "st" ]], "stencil/decorators-context": 2, "stencil/element-type": 2, "stencil/host-data-deprecated": 2, "stencil/methods-must-be-public": 2, "stencil/no-unused-watch": 2, "stencil/prefer-vdom-listener": 2, "stencil/props-must-be-public": 2, "stencil/render-returns-host": 2, "stencil/reserved-member-names": 2, "stencil/single-export": 2 } }], settings: { "react": { "version": "stencil-maintainers-put-an-invalid-version-intentionally-if-this-errors-please-raise-an-issue-https://github.com/stenciljs/eslint-plugin/issues" } } }; //#endregion //#region src/configs/recommended.ts var recommended_default = { plugins: ["react"], extends: ["plugin:stencil/base"],