UNPKG

@stencil/eslint-plugin

Version:

ESLint rules specific to Stencil JS projects.

1,550 lines 51.5 kB
import { t as configs_default } from "./configs-Ciyc5czw.js"; import { createRequire } from "node:module"; 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; /** * Checks if an ESTree node has private or protected accessibility. */ function isPrivateESTree(node) { if (node.accessibility === "private" || node.accessibility === "protected") return true; return node.key?.type === "PrivateIdentifier"; } /** * Checks if an ESTree node has any Stencil decorator. */ function hasStencilDecorator(node) { return getDecorator(node).some((dec) => stencilDecorators.includes(decoratorName(dec))); } /** * Returns the JSDoc block comments attached to a node using ESTree sourceCode APIs. * Each returned object has `value` (the raw comment text) and parsed `tags`. */ function getJSDocComments(node, sourceCode) { let comments = sourceCode.getCommentsBefore(node); if ((!comments || comments.length === 0) && node.decorators && node.decorators.length > 0) comments = sourceCode.getCommentsBefore(node.decorators[0]); return comments.filter((c) => c.type === "Block" && c.value.startsWith("*")).map((c) => { const tags = []; const tagRegex = /@(\w+)\s*(.*)/g; let match; while ((match = tagRegex.exec(c.value)) !== null) tags.push({ tagName: match[1], comment: match[2].trim() }); return { value: c.value, tags }; }); } 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 = 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" ]; 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, 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, SyntaxKind.UniqueKeyword, SyntaxKind.KeyOfKeyword, SyntaxKind.NewKeyword, SyntaxKind.ImportKeyword, SyntaxKind.ReadonlyKeyword; //#endregion //#region src/rules/async-methods.ts const rule$25 = { 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; if (!parserServices?.esTreeNodeToTSNodeMap || !parserServices?.program) return { ...stencil.rules }; 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); if (!isThenableType(typeChecker, method, typeChecker.getReturnTypeOfSignature(signature))) { 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); } }); } } }; } }; //#endregion //#region src/rules/ban-default-true.ts const rule$24 = { 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` }); } }; } }; //#endregion //#region src/rules/ban-prefix.ts const DEFAULTS$2 = [ "stencil", "stnl", "st" ]; const rule$23 = { 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) { return { ...stencilComponentContext().rules, ClassDeclaration: (node) => { const component = getDecorator(node, "Component"); if (!component) return; const [opts] = parseDecorator(component); if (!opts || !opts.tag) return; const tag = opts.tag; if ((context.options[0] || DEFAULTS$2).some((t) => tag.startsWith(`${t}-`))) context.report({ node, message: `The component with tag name ${tag} have a banned prefix.` }); } }; } }; //#endregion //#region src/rules/class-pattern.ts const rule$22 = { 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) { return { ...stencilComponentContext().rules, ClassDeclaration: (node) => { const component = getDecorator(node, "Component"); const options = context.options[0]; const { pattern, ignoreCase } = options || {}; if (!component || !options || !pattern) return; const className = node.id?.name; if (!className) return; 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}).` }); } } }; } }; //#endregion //#region src/rules/decorators-context.ts const rule$21 = { 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.` }); } } } }; } }; //#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$20 = { 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 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 node = decorator.parent; const decoratorEndLine = decorator.loc.end.line; const memberStartLine = node.key.loc.start.line; const isOnSameLineAsMember = decoratorEndLine === memberStartLine; if (config === "multiline") { const allDecorators = node.decorators || []; const indexInAll = allDecorators.indexOf(decorator); const nextDecorator = indexInAll >= 0 ? allDecorators[indexInAll + 1] : void 0; if (decoratorEndLine === (nextDecorator ? nextDecorator.loc.start.line : memberStartLine)) context.report({ node, message: `The @${decName} decorator can only be applied as multiline.` }); } else if (config === "inline" && !isOnSameLineAsMember) context.report({ node, message: `The @${decName} decorator can only be applied as inline.` }); } function getStyle(node) { if (!stencil.isComponent() || !options || !Object.keys(options).length) return; getDecorator(node).filter((dec) => stencilDecorators.includes(decoratorName(dec))).forEach(checkStyle); } return { ...stencil.rules, PropertyDefinition: getStyle, "MethodDefinition[kind=method]": getStyle }; } }; //#endregion //#region src/rules/element-type.ts const rule$19 = { 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 [opts] = parseDecorator(getDecorator(node.parent.parent.parent, "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); } }); } } }; } }; //#endregion //#region src/rules/host-data-deprecated.ts const rule$18 = { 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.` }); } }; } }; //#endregion //#region src/rules/methods-must-be-public.ts const rule$17 = { 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(); return { ...stencil.rules, "MethodDefinition[kind=method]": (node) => { if (stencil.isComponent() && getDecorator(node, "Method")) { if (isPrivateESTree(node)) context.report({ node, message: `Class methods decorated with @Method() cannot be private nor protected` }); } } }; } }; //#endregion //#region src/rules/no-unused-watch.ts const varsList = /* @__PURE__ */ new Set(); const rule$16 = { 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(); function getVars(node) { if (!stencil.isComponent()) return; const memberNode = node.parent; const varName = memberNode.key?.name || memberNode.key?.value; if (varName) varsList.add(varName); } function checkWatch(node) { if (!stencil.isComponent()) return; const varName = node.expression?.arguments?.[0]?.value; if (varName && !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 RESERVED_PUBLIC_ATTRIBUTES = new Set([...[ "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" ]].map((p) => p.toLowerCase())); function isReservedAttribute(attributeName) { return RESERVED_PUBLIC_ATTRIBUTES.has(attributeName.toLowerCase()); } //#endregion //#region src/rules/own-methods-must-be-private.ts const rule$15 = { 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(); return { ...stencil.rules, "MethodDefinition[kind=method]": (node) => { if (!stencil.isComponent()) return; const methodName = node.key.name || node.key.value; const isStencilCycle = stencilLifecycle.includes(methodName); if (!hasStencilDecorator(node) && !isStencilCycle && !isPrivateESTree(node)) context.report({ node, message: `Own class methods cannot be public`, fix(fixer) { const publicToken = context.sourceCode.getTokens(node).find((token) => token.value === "public"); if (publicToken) return fixer.replaceText(publicToken, "private"); else return fixer.insertTextBefore(node.key, "private "); } }); } }; } }; //#endregion //#region src/rules/own-props-must-be-private.ts const rule$14 = { 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(); return { ...stencil.rules, PropertyDefinition: (node) => { if (!stencil.isComponent()) return; if (!hasStencilDecorator(node) && !isPrivateESTree(node)) context.report({ node, message: `Own class properties cannot be public`, fix(fixer) { const publicToken = context.sourceCode.getTokens(node).find((token) => token.value === "public"); if (publicToken) return fixer.replaceText(publicToken, "private"); else return fixer.insertTextBefore(node.key, "private "); } }); } }; } }; //#endregion //#region src/rules/prefer-vdom-listener.ts const rule$13 = { 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 = 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" ]; //#endregion //#region src/rules/props-must-be-public.ts const rule$12 = { 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(); return { ...stencil.rules, PropertyDefinition: (node) => { if (stencil.isComponent() && getDecorator(node, "Prop")) { if (isPrivateESTree(node)) context.report({ node, message: `Class properties decorated with @Prop() cannot be private nor protected` }); } } }; } }; //#endregion //#region src/rules/props-must-be-readonly.ts const rule$11 = { 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(); return { ...stencil.rules, PropertyDefinition: (node) => { const propDecorator = getDecorator(node, "Prop"); if (stencil.isComponent() && propDecorator) { const [opts] = parseDecorator(propDecorator); if (opts && opts.mutable === true) return; if (!node.readonly) context.report({ node: node.key, message: `Class properties decorated with @Prop() should be readonly`, fix(fixer) { return fixer.insertTextBefore(node.key, "readonly "); } }); } } }; } }; //#endregion //#region src/rules/render-returns-host.ts const rule$10 = { 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; if (!parserServices?.esTreeNodeToTSNodeMap || !parserServices?.program) return { ...stencil.rules }; 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.` }); } }; } }; //#endregion //#region src/rules/required-jsdoc.ts const DECORATORS = [ "Prop", "Method", "Event" ]; const INVALID_TAGS = ["type", "memberof"]; const rule$9 = { 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(); function getJSDoc(node) { if (!stencil.isComponent()) return; DECORATORS.forEach((decName) => { if (getDecorator(node, decName)) { const jsDocs = getJSDocComments(node, context.sourceCode); const isValid = jsDocs.length > 0; const haveTags = isValid && jsDocs.some((jsdoc) => jsdoc.tags.length > 0 && jsdoc.tags.some((tag) => INVALID_TAGS.includes(tag.tagName.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 }; } }; //#endregion //#region src/rules/required-prefix.ts const rule$8 = { 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) { return { ...stencilComponentContext().rules, ClassDeclaration: (node) => { const component = getDecorator(node, "Component"); if (!component) return; const [{ tag }] = parseDecorator(component); if (!context.options[0].some((t) => tag.startsWith(t))) context.report({ node, message: `The component with tagName ${tag} have not a valid prefix.` }); } }; } }; //#endregion //#region src/rules/reserved-member-names.ts const rule$7 = { 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 = 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 }; } }; 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()); } //#endregion //#region src/rules/single-export.ts const 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 TYPE_ONLY_DECLARATIONS = new Set(["TSInterfaceDeclaration", "TSTypeAliasDeclaration"]); /** Finds every top-level, @Component()-decorated class -- exported or not. */ function findComponentClasses(body) { const found = []; for (const node of body) { const candidate = node.type === "ClassDeclaration" ? node : node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" ? node.declaration : void 0; if (candidate?.type === "ClassDeclaration" && getDecorator(candidate, "Component")) found.push(candidate); } return found; } /** * Local names bound to type-only declarations/imports at the top level of the module. * Used as a fallback for resolving bare `export { X }` specifiers when no type checker * is available -- with a checker, `isSpecifierTypeOnly` resolves this precisely instead. */ function collectTypeOnlyNames(body) { const names = /* @__PURE__ */ new Set(); for (const node of body) if (TYPE_ONLY_DECLARATIONS.has(node.type)) names.add(node.id.name); else if (node.type === "ImportDeclaration") { for (const specifier of node.specifiers) if (node.importKind === "type" || specifier.type === "ImportSpecifier" && specifier.importKind === "type") names.add(specifier.local.name); } return names; } /** * `export { X }` resolves to an alias symbol pointing at X's declaration, not a symbol * with X's own flags -- has to be resolved through the alias to see whether X is a type. */ function isTypeOnlySymbol(symbol, typeChecker) { return (((symbol.flags & ts.SymbolFlags.Alias) !== 0 ? typeChecker.getAliasedSymbol(symbol) : symbol).flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias)) !== 0; } /** * Resolves whether a bare `export { X }` specifier is bound to a type. Prefers the type * checker when available -- `getSymbolAtLocation` on the specifier's local identifier is * always safe because that identifier lives in the file being linted, so this never runs * into the "declaration lives in another file" problem that made a symbol-first approach * (walking `getExportsOfModule` results back to their declarations) unreliable for * re-exports. Falls back to the same-file syntactic heuristic when no checker is present. */ function isSpecifierTypeOnly(specifier, typeOnlyNames, typeChecker, parserServices) { if (typeChecker && parserServices) { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(specifier.local); const symbol = tsNode && typeChecker.getSymbolAtLocation(tsNode); if (symbol) return isTypeOnlySymbol(symbol, typeChecker); } return typeOnlyNames.has(specifier.local.name); } function collectExportedItems(body, typeOnlyNames, typeChecker, parserServices) { const items = []; for (const node of body) if (node.type === "ExportNamedDeclaration") { if (node.exportKind === "type") continue; if (node.source) { if (node.specifiers.length > 0) items.push({ node }); continue; } if (node.declaration) { if (TYPE_ONLY_DECLARATIONS.has(node.declaration.type)) continue; if (node.declaration.type === "VariableDeclaration") for (const declarator of node.declaration.declarations) { const isIdentifier = declarator.id.type === "Identifier"; items.push({ name: isIdentifier ? declarator.id.name : void 0, node: isIdentifier ? declarator.id : declarator }); } else items.push({ name: node.declaration.id?.name, node: node.declaration }); } else for (const specifier of node.specifiers) { if (specifier.exportKind === "type" || isSpecifierTypeOnly(specifier, typeOnlyNames, typeChecker, parserServices)) continue; items.push({ name: specifier.local.name, node: specifier }); } } else if (node.type === "ExportDefaultDeclaration") { if (TYPE_ONLY_DECLARATIONS.has(node.declaration.type)) continue; items.push({ name: node.declaration.type === "Identifier" ? node.declaration.name : node.declaration.id?.name, node: node.declaration }); } else if (node.type === "ExportAllDeclaration") { if (node.exportKind === "type") continue; items.push({ node }); } return items; } const rule$6 = { 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?.esTreeNodeToTSNodeMap && parserServices?.program ? parserServices.program.getTypeChecker() : void 0; return { Program: (program) => { const body = program.body; const componentClasses = findComponentClasses(body); if (componentClasses.length === 0) return; const exportedItems = collectExportedItems(body, collectTypeOnlyNames(body), typeChecker, parserServices); for (const component of componentClasses) for (const item of exportedItems) { if (item.node === component || item.name && item.name === component.id?.name) continue; context.report({ node: item.node, message: MESSAGE }); } } }; } }; //#endregion //#region src/rules/strict-mutable.ts const mutableProps = /* @__PURE__ */ new Map(); const mutableAssigned = /* @__PURE__ */ new Set(); const rule$5 = { 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); if (parsed && parsed.length && parsed[0].mutable || false) { 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(); } } }; } }; //#endregion //#region src/rules/ban-side-effects.ts const rule$4 = { 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.filename); 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" ]; //#endregion //#region src/rules/strict-boolean-conditions.ts /** * @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$3 = { 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; if (!parserServices?.esTreeNodeToTSNodeMap || !parserServices?.program) return {}; const program = parserServices.program; const options = parseOptions(context.options[0] || [ "allow-null-union", "allow-undefined-union", "allow-boolean-or-undefined" ], 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 === 0 && !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) => { walk(parserServices.esTreeNodeToTSNodeMap.get(node)); } }; } }; 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 : 0; case false: return isTypeFlagSet(type, ts$1.TypeFlags.BooleanLiteral) ? void 0 : 1; case void 0: return; } } 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; return isTruthy; } function handleUnion(type, options) { if (options.allowBooleanOrUndefined) switch (isBooleanUndefined(type)) { case true: return; case false: return 1; } for (const ty of type.types) { const failure = failureForKind(getKind(ty), true, options); if (failure !== void 0) return failure; } } /** Fails if a kind of falsiness is not allowed. */ function failureForKind(kind, isInUnion, options) { switch (kind) { case 0: case 1: return options.allowString ? void 0 : 2; case 2: case 3: return options.allowNumber ? void 0 : 3; case 8: return options.allowEnum ? void 0 : 6; case 10: return 8; case 6: return isInUnion && !options.allowNullUnion ? 4 : void 0; case 7: return isInUnion && !options.allowUndefinedUnion ? 5 : void 0; default: return; } } /** Divides a type into always true, always false, or unknown. */ function triState(kind) { switch (kind) { case 0: case 2: case 4: case 8: return; case 6: case 7: case 3: case 1: case 5: return false; case 9: case 10: return true; } } function getKind(type) { return is(ts$1.TypeFlags.StringLike) ? 0 : is(ts$1.TypeFlags.NumberLike) ? 2 : is(ts$1.TypeFlags.Boolean) ? 4 : isObject("Promise") ? 10 : is(ts$1.TypeFlags.Null) ? 6 : is(ts$1.TypeFlags.Undefined | ts$1.TypeFlags.Void) ? 7 : is(ts$1.TypeFlags.EnumLike) ? 8 : is(ts$1.TypeFlags.BooleanLiteral) ? type.intrinsicName === "true" ? 9 : 5 : 9; 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; } } 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 0: 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 1: return "is always falsy"; case 2: return `${is} a string`; case 3: return `${is} a number`; case 4: return `${is} null`; case 5: return `${is} undefined`; case 6: return `${is} an enum`; case 8: return "promise handled as boolean expression"; case 7: return "unions more than one truthy/falsy type"; } } function isTypeFlagSet(obj, flag) { return (obj.flags & flag) !== 0; } //#endregion //#region src/rules/ban-exported-const-enums.ts const rule$2 = { 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` }); } }; } }; //#endregion //#region src/rules/dependency-suggestions.ts const rule$1 = { meta: { docs: { description: "This rule can provide suggestions about dependencies in stencil apps", recommended: true }, schema: [], type: "suggestion" }, create(context) { return { ImportDeclaration: (node) => { const message = SUGGESTIONS[node.source.value]; 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` }; //#endregion //#region src/index.ts /** * @fileoverview ESLint rules specific to Stencil JS projects. * @author Tom Chinery &lt;tom.chinery@addtoevent.co.uk&gt; */ const plugin = { rules: { "ban-side-effects": rule$4, "ban-default-true": rule$24, "ban-exported-const-enums": rule$2, "dependency-suggestions": rule$1, "enforce-slot-jsdoc": { meta: { docs: { description: "Ensures slots are documented with JSDoc.", category: "Possible Errors", recommended: true }, schema: [], type: "problem" }, create(context) { const stencil = stencilComponentContext(); const implementedSlots = /* @__PURE__ */ new Set(); return { ...stencil.rules, "ClassDeclaration:exit": (node) => { if (stencil.isComponent()) { const jsDocs = getJSDocComments(node, context.sourceCode); const documentedSlots = new Set((jsDocs.length && jsDocs[0].tags.length ? jsDocs[0].tags.filter((tag) => tag.tagName === "slot") : []).map((tag) => { const c = tag.comment.trim(); if (!c || c === "-" || c.startsWith("- ")) return "<default>"; const idx = c.indexOf(" - "); return idx === -1 ? c : c.slice(0, idx).trim(); })); const missingDocSlots = Array.from(implementedSlots).filter((slot) => !documentedSlots.has(slot)); const nonImplementedSlots = Array.from(documentedSlots).filter((slot) => !implementedSlots.has(slot)); missingDocSlots.forEach((slot) => { context.report({ node, message: slot === "<default>" ? "The default @slot must be documented." : `The @slot '${slot}' must be documented.` }); }); nonImplementedSlots.forEach((slot) => { context.report({ node, message: slot === "<default>" ? "The default @slot is not implemented." : `The @slot '${slot}' is not implemented.` }); }); } implementedSlots.clear(); stencil.rules["ClassDeclaration:exit"](node); }, JSXElement(node) { if (node.openingElement.name.name !== "slot") return; const nameAttribute = node.openingElement.attributes.find((attribute) => attribute.name.name === "name"); const slotName = nameAttribute && nameAttribute.value ? nameAttribute.value.value : "<default>"; implementedSlots.add(slotName); } }; } }, "strict-boolean-conditions": rule$3, "async-methods": rule$25, "ban-prefix": rule$23, "class-pattern": rule$22, "decorators-context": rule$21, "decorators-style": rule$20, "element-type": rule$19, "host-data-deprecated": rule$18, "methods-must-be-public": rule$17, "no-unused-watch": rule$16, "own-methods-must-be-private": rule$15, "own-props-must-be-private": rule$14, "prefer-vdom-listener": rule$13, "props-must-be-public": rule$12, "props-must-be-readonly": rule$11, "render-returns-host": rule$10, "required-jsdoc": rule$9, "required-prefix": rule$8