@stencil-community/eslint-plugin
Version:
ESLint rules specific to Stencil JS projects.
1,383 lines (1,356 loc) • 70.2 kB
JavaScript
'use strict';
var react = require('eslint-plugin-react');
var ts = require('typescript');
var eslintUtils = require('eslint-utils');
var tsutils = require('tsutils');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var ts__namespace = /*#__PURE__*/_interopNamespaceDefault(ts);
const SyntaxKind = ts.SyntaxKind;
function isPrivate(originalNode) {
const modifiers = ts.canHaveModifiers(originalNode)
? ts.getModifiers(originalNode)
: undefined;
if (modifiers) {
return modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword ||
m.kind === ts.SyntaxKind.ProtectedKeyword);
}
// detect private identifier (#)
const firstChildNode = originalNode.getChildAt(0);
return firstChildNode ? firstChildNode.kind === SyntaxKind.PrivateIdentifier : false;
}
function getDecoratorList(originalNode) {
const decorators = ts.canHaveDecorators(originalNode)
? ts.getDecorators(originalNode)
: undefined;
return decorators;
}
function getDecorator(node, decoratorName) {
if (decoratorName) {
return node.decorators && node.decorators.find(isDecoratorNamed(decoratorName));
}
return node.decorators ? node.decorators.filter((dec) => dec.expression) : [];
}
function parseDecorator(decorator) {
if (decorator && decorator.expression && decorator.expression.type === 'CallExpression') {
return decorator.expression.arguments.map((a) => {
const parsed = eslintUtils.getStaticValue(a);
return parsed ? parsed.value : undefined;
});
}
return [];
}
function decoratorName(dec) {
return dec.expression && dec.expression.callee.name;
}
function isDecoratorNamed(propName) {
return (dec) => {
return decoratorName(dec) === propName;
};
}
function stencilComponentContext() {
let componentNode;
return {
rules: {
'ClassDeclaration': (node) => {
const component = getDecorator(node, 'Component');
if (component) {
componentNode = component;
}
},
'ClassDeclaration:exit': (node) => {
if (componentNode === node) {
componentNode = undefined;
}
}
},
isComponent() {
return !!componentNode;
}
};
}
function getType(node) {
return node.typeAnnotation?.typeAnnotation?.typeName?.name;
}
const stencilDecorators = ['Component', 'Prop', 'State', 'Watch', 'Element', 'Method', 'Event', 'Listen', 'AttachInternals'];
const stencilLifecycle = [
'connectedCallback',
'disconnectedCallback',
'componentWillLoad',
'componentDidLoad',
'componentWillRender',
'componentDidRender',
'componentShouldUpdate',
'componentWillUpdate',
'componentDidUpdate',
'formAssociatedCallback',
'formDisabledCallback',
'formResetCallback',
'formStateRestoreCallback',
'render'
];
({
[SyntaxKind.OpenBraceToken]: '{',
[SyntaxKind.CloseBraceToken]: '}',
[SyntaxKind.OpenParenToken]: '(',
[SyntaxKind.CloseParenToken]: ')',
[SyntaxKind.OpenBracketToken]: '[',
[SyntaxKind.CloseBracketToken]: ']',
[SyntaxKind.DotToken]: '.',
[SyntaxKind.DotDotDotToken]: '...',
[SyntaxKind.SemicolonToken]: ',',
[SyntaxKind.CommaToken]: ',',
[SyntaxKind.LessThanToken]: '<',
[SyntaxKind.GreaterThanToken]: '>',
[SyntaxKind.LessThanEqualsToken]: '<=',
[SyntaxKind.GreaterThanEqualsToken]: '>=',
[SyntaxKind.EqualsEqualsToken]: '==',
[SyntaxKind.ExclamationEqualsToken]: '!=',
[SyntaxKind.EqualsEqualsEqualsToken]: '===',
[SyntaxKind.InstanceOfKeyword]: 'instanceof',
[SyntaxKind.ExclamationEqualsEqualsToken]: '!==',
[SyntaxKind.EqualsGreaterThanToken]: '=>',
[SyntaxKind.PlusToken]: '+',
[SyntaxKind.MinusToken]: '-',
[SyntaxKind.AsteriskToken]: '*',
[SyntaxKind.AsteriskAsteriskToken]: '**',
[SyntaxKind.SlashToken]: '/',
[SyntaxKind.PercentToken]: '%',
[SyntaxKind.PlusPlusToken]: '++',
[SyntaxKind.MinusMinusToken]: '--',
[SyntaxKind.LessThanLessThanToken]: '<<',
[SyntaxKind.LessThanSlashToken]: '</',
[SyntaxKind.GreaterThanGreaterThanToken]: '>>',
[SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: '>>>',
[SyntaxKind.AmpersandToken]: '&',
[SyntaxKind.BarToken]: '|',
[SyntaxKind.CaretToken]: '^',
[SyntaxKind.ExclamationToken]: '!',
[SyntaxKind.TildeToken]: '~',
[SyntaxKind.AmpersandAmpersandToken]: '&&',
[SyntaxKind.BarBarToken]: '||',
[SyntaxKind.QuestionToken]: '?',
[SyntaxKind.ColonToken]: ':',
[SyntaxKind.EqualsToken]: '=',
[SyntaxKind.PlusEqualsToken]: '+=',
[SyntaxKind.MinusEqualsToken]: '-=',
[SyntaxKind.AsteriskEqualsToken]: '*=',
[SyntaxKind.AsteriskAsteriskEqualsToken]: '**=',
[SyntaxKind.SlashEqualsToken]: '/=',
[SyntaxKind.PercentEqualsToken]: '%=',
[SyntaxKind.LessThanLessThanEqualsToken]: '<<=',
[SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>=',
[SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=',
[SyntaxKind.AmpersandEqualsToken]: '&=',
[SyntaxKind.BarEqualsToken]: '|=',
[SyntaxKind.CaretEqualsToken]: '^=',
[SyntaxKind.AtToken]: '@',
[SyntaxKind.InKeyword]: 'in',
[SyntaxKind.UniqueKeyword]: 'unique',
[SyntaxKind.KeyOfKeyword]: 'keyof',
[SyntaxKind.NewKeyword]: 'new',
[SyntaxKind.ImportKeyword]: 'import',
[SyntaxKind.ReadonlyKeyword]: 'readonly'
});
const rule$o = {
meta: {
docs: {
description: 'This rule catches Stencil public methods that are not async.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem',
fixable: 'code'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
const typeChecker = parserServices.program.getTypeChecker();
return {
...stencil.rules,
'MethodDefinition > Decorator[expression.callee.name=Method]': (decoratorNode) => {
if (!stencil.isComponent()) {
return;
}
const node = decoratorNode.parent;
const method = parserServices.esTreeNodeToTSNodeMap.get(node);
const signature = typeChecker.getSignatureFromDeclaration(method);
const returnType = typeChecker.getReturnTypeOfSignature(signature);
if (!tsutils.isThenableType(typeChecker, method, returnType)) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const text = String(originalNode.getFullText());
context.report({
node: node.key,
message: `External @Method() ${node.key.name}() must return a Promise. Consider prefixing the method with async, such as @Method() async ${node.key.name}().`,
fix(fixer) {
const result = text
// a newline + whitespace preceding `@Method` may be captured, remove it
.trimLeft()
// capture the number of following the decorator to know how far to indent the `async` method
.replace(/@Method\(\)\n(\s*)/, '@Method()\n$1async ')
// replace any inlined @Method decorators
.replace('@Method() ', '@Method() async')
// swap the order of the `async` and `public` keywords
.replace('async public', 'public async')
// swap the order of the `async` and `private` keywords
.replace('async private', 'private async');
return fixer.replaceText(node, result);
}
});
}
}
};
}
};
const rule$n = {
meta: {
docs: {
description: 'This rule catches Stencil Props defaulting to true.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem',
},
create(context) {
const stencil = stencilComponentContext();
return {
...stencil.rules,
'PropertyDefinition': (node) => {
const propDecorator = getDecorator(node, 'Prop');
if (!(stencil.isComponent() && propDecorator)) {
return;
}
if (node.value?.value === true) {
context.report({
node: node,
message: `Boolean properties decorated with @Prop() should default to false`
});
}
}
};
}
};
const DEFAULTS$2 = ['stencil', 'stnl', 'st'];
const rule$m = {
meta: {
docs: {
description: 'This rule catches usages banned prefix in component tag name.',
category: 'Possible Errors',
recommended: true
},
schema: [
{
type: 'array',
items: {
type: 'string'
},
minLength: 1,
additionalProperties: false
}
],
type: 'problem'
},
create(context) {
const stencil = stencilComponentContext();
return {
...stencil.rules,
'ClassDeclaration': (node) => {
const component = getDecorator(node, 'Component');
if (!component) {
return;
}
const [opts] = parseDecorator(component);
if (!opts || !opts.tag) {
return;
}
const tag = opts.tag;
const options = context.options[0] || DEFAULTS$2;
const match = options.some((t) => tag.startsWith(`${t}-`));
if (match) {
context.report({
node: node,
message: `The component with tag name ${tag} have a banned prefix.`
});
}
}
};
}
};
const rule$l = {
meta: {
docs: {
description: 'This rule catches usages of non valid class names.',
category: 'Possible Errors',
recommended: false
},
schema: [
{
type: 'object',
properties: {
pattern: {
type: 'string'
},
ignoreCase: {
type: 'boolean'
}
},
additionalProperties: false
}
],
type: 'problem'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
'ClassDeclaration': (node) => {
const component = getDecorator(node, 'Component');
const options = context.options[0];
const { pattern, ignoreCase } = options || {};
if (!component || !options || !pattern) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const className = originalNode.symbol.escapedName;
const regExp = new RegExp(pattern, ignoreCase ? 'i' : undefined);
if (!regExp.test(className)) {
const [opts] = parseDecorator(component);
if (!opts || !opts.tag) {
return;
}
context.report({
node: node,
message: `The class name in component with tag name ${opts.tag} is not valid (${regExp}).`
});
}
}
};
}
};
const rule$k = {
meta: {
docs: {
description: 'This rule catches Stencil Decorators used in incorrect locations.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
const stencil = stencilComponentContext();
return {
...stencil.rules,
'Decorator': (node) => {
if (!stencil.isComponent()) {
return;
}
if (node.expression && node.expression.callee) {
const decName = node.expression.callee.name;
if (decName === 'Prop' ||
decName === 'State' ||
decName === 'Element' ||
decName === 'Event') {
if (node.parent.type !== 'PropertyDefinition' &&
(node.parent.type === 'MethodDefinition' &&
['get', 'set'].indexOf(node.parent.kind) < 0)) {
context.report({
node: node,
message: `The @${decName} decorator can only be applied to class properties.`
});
}
}
else if (decName === 'Method' ||
decName === 'Watch' ||
decName === 'Listen') {
if (node.parent.type !== 'MethodDefinition') {
context.report({
node: node,
message: `The @${decName} decorator can only be applied to class methods.`
});
}
}
else if (decName === 'Component') {
if (node.parent.type !== 'ClassDeclaration') {
context.report({
node: node,
message: `The @${decName} decorator can only be applied to a class.`
});
}
}
}
}
};
}
};
const ENUMERATE = ['inline', 'multiline', 'ignore'];
const DEFAULTS$1 = {
prop: 'ignore',
state: 'ignore',
element: 'ignore',
event: 'ignore',
method: 'ignore',
watch: 'ignore',
listen: 'ignore'
};
const rule$j = {
meta: {
docs: {
description: 'This rule catches Stencil Decorators not used in consistent style.',
category: 'Possible Errors',
recommended: true
},
schema: [
{
type: 'object',
properties: {
prop: {
type: 'string',
enum: ENUMERATE
},
state: {
type: 'string',
enum: ENUMERATE
},
element: {
type: 'string',
enum: ENUMERATE
},
event: {
type: 'string',
enum: ENUMERATE
},
method: {
type: 'string',
enum: ENUMERATE
},
watch: {
type: 'string',
enum: ENUMERATE
},
listen: {
type: 'string',
enum: ENUMERATE
}
}
}
],
type: 'layout'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
const opts = context.options[0] || {};
const options = { ...DEFAULTS$1, ...opts };
function checkStyle(decorator) {
const decName = decoratorName(decorator);
const config = options[decName.toLowerCase()];
if (!config || config === 'ignore') {
return;
}
const decoratorNode = parserServices.esTreeNodeToTSNodeMap.get(decorator);
const decoratorText = decoratorNode.getText()
.replace('(', '\\(')
.replace(')', '\\)');
const text = decoratorNode.parent.getText();
const separator = config === 'multiline' ? '\\r?\\n' : ' ';
const regExp = new RegExp(`${decoratorText}${separator}`, 'i');
if (!regExp.test(text)) {
const node = decorator.parent;
context.report({
node: node,
message: `The @${decName} decorator can only be applied as ${config}.`,
});
}
}
function getStyle(node) {
if (!stencil.isComponent() || !options || !Object.keys(options).length) {
return;
}
const decorators = getDecorator(node);
decorators.filter((dec) => stencilDecorators.includes(decoratorName(dec))).forEach(checkStyle);
}
return {
...stencil.rules,
'PropertyDefinition': getStyle,
'MethodDefinition[kind=method]': getStyle
};
}
};
const rule$i = {
meta: {
docs: {
description: 'This rule catches Stencil Element type not matching tag name.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem',
fixable: 'code'
},
create(context) {
const stencil = stencilComponentContext();
function parseTag(tag) {
let result = tag[0].toUpperCase() + tag.slice(1);
const tagBody = tag.split('-');
if (tagBody.length > 1) {
result = tagBody.map((tpart) => tpart[0].toUpperCase() + tpart.slice(1)).join('');
}
return result;
}
return {
...stencil.rules,
'PropertyDefinition > Decorator[expression.callee.name=Element]': (node) => {
if (stencil.isComponent()) {
const tagType = getType(node.parent);
const component = getDecorator(node.parent.parent.parent, 'Component');
const [opts] = parseDecorator(component);
if (!opts || !opts.tag) {
return;
}
const parsedTag = `HTML${parseTag(opts.tag)}Element`;
if (tagType !== parsedTag) {
context.report({
node: node.parent.typeAnnotation ?? node.parent,
message: `@Element type is not matching tag for component (${parsedTag})`,
fix(fixer) {
// If the property has a type annotation, we can replace just that node with the parsed tag
// @Element() elm: HTMLElement; -> @Element() elm: HTMLSampleTagElement;
if (node.parent.typeAnnotation?.typeAnnotation) {
return fixer.replaceText(node.parent.typeAnnotation.typeAnnotation, parsedTag);
}
// If no type annotation exists on the property, we'll do some string manipulation to insert one.
// @Element() elm; -> @Element() elm: HTMLSampleTagElement;
const text = context.sourceCode.getText(node.parent).replace(';', '').concat(`: ${parsedTag};`);
return fixer.replaceText(node.parent, text);
}
});
}
}
}
};
}
};
/**
* @fileoverview ESLint rules specific to Stencil JS projects.
* @author Tom Chinery <tom.chinery@addtoevent.co.uk>
*/
const rule$h = {
meta: {
docs: {
description: 'This rule catches usage of hostData method.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
const stencil = stencilComponentContext();
return {
...stencil.rules,
'MethodDefinition[key.name=hostData]': (node) => {
if (stencil.isComponent()) {
context.report({
node: node.key,
message: `hostData() is deprecated and <Host> should be used in the render function instead.`
});
}
}
};
}
};
const rule$g = {
meta: {
docs: {
description: 'This rule catches Stencil Methods marked as private or protected.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
'MethodDefinition[kind=method]': (node) => {
if (stencil.isComponent() && getDecorator(node, 'Method')) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
if (isPrivate(originalNode)) {
context.report({
node: node,
message: `Class methods decorated with @Method() cannot be private nor protected`
});
}
}
}
};
}
};
const varsList = new Set();
const rule$f = {
meta: {
docs: {
description: 'This rule catches Stencil Watch for not defined variables in Prop or State.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'suggestion'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
function getVars(node) {
if (!stencil.isComponent()) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const varName = originalNode.parent.name.escapedText;
varsList.add(varName);
}
function checkWatch(node) {
if (!stencil.isComponent()) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const varName = originalNode.expression.arguments[0].text;
if (!varsList.has(varName) && !isReservedAttribute(varName.toLowerCase())) {
context.report({
node: node,
message: `Watch decorator @Watch("${varName}") is not matching with any @Prop() or @State()`,
});
}
}
return {
ClassDeclaration: stencil.rules.ClassDeclaration,
'PropertyDefinition > Decorator[expression.callee.name=Prop]': getVars,
'MethodDefinition[kind=get] > Decorator[expression.callee.name=Prop]': getVars,
'MethodDefinition[kind=set] > Decorator[expression.callee.name=Prop]': getVars,
'PropertyDefinition > Decorator[expression.callee.name=State]': getVars,
'MethodDefinition[kind=method] > Decorator[expression.callee.name=Watch]': checkWatch,
'ClassDeclaration:exit': (node) => {
if (!stencil.isComponent()) {
return;
}
stencil.rules['ClassDeclaration:exit'](node);
varsList.clear();
}
};
}
};
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
const GLOBAL_ATTRIBUTES$1 = [
'about',
'accessKey',
'autocapitalize',
'autofocus',
'class',
'contenteditable',
'contextmenu',
'dir',
'draggable',
'enterkeyhint',
'hidden',
'id',
'inert',
'inputmode',
'id',
'itemid',
'itemprop',
'itemref',
'itemscope',
'itemtype',
'lang',
'nonce',
'part',
'popover',
'role',
'slot',
'spellcheck',
'style',
'tabindex',
'title',
'translate',
'virtualkeyboardpolicy',
];
const RESERVED_PUBLIC_ATTRIBUTES = new Set([
...GLOBAL_ATTRIBUTES$1,
].map(p => p.toLowerCase()));
function isReservedAttribute(attributeName) {
return RESERVED_PUBLIC_ATTRIBUTES.has(attributeName.toLowerCase());
}
const rule$e = {
meta: {
docs: {
description: "This rule catches own class methods marked as public.",
category: "Possible Errors",
recommended: true,
},
schema: [],
type: 'problem',
fixable: 'code',
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
"MethodDefinition[kind=method]": (node) => {
if (!stencil.isComponent()) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const decorators = getDecoratorList(originalNode);
const stencilDecorator = decorators &&
decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText));
const stencilCycle = stencilLifecycle.includes(originalNode.name.escapedText);
if (!stencilDecorator && !stencilCycle && !isPrivate(originalNode)) {
context.report({
node: node,
message: `Own class methods cannot be public`,
fix(fixer) {
const sourceCode = context.getSourceCode();
const tokens = sourceCode.getTokens(node);
const publicToken = tokens.find(token => token.value === 'public');
if (publicToken) {
return fixer.replaceText(publicToken, 'private');
}
else {
return fixer.insertTextBefore(node.key, 'private ');
}
}
});
}
},
};
},
};
const rule$d = {
meta: {
docs: {
description: "This rule catches own class attributes marked as public.",
category: "Possible Errors",
recommended: true,
},
schema: [],
type: 'problem',
fixable: 'code',
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
PropertyDefinition: (node) => {
if (!stencil.isComponent()) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const decorators = getDecoratorList(originalNode);
const stencilDecorator = decorators &&
decorators.some((dec) => stencilDecorators.includes(dec.expression.expression.escapedText));
if (!stencilDecorator && !isPrivate(originalNode)) {
context.report({
node: node,
message: `Own class properties cannot be public`,
fix(fixer) {
const sourceCode = context.getSourceCode();
const tokens = sourceCode.getTokens(node);
const publicToken = tokens.find(token => token.value === 'public');
if (publicToken) {
return fixer.replaceText(publicToken, 'private');
}
else {
return fixer.insertTextBefore(node.key, 'private ');
}
}
});
}
},
};
},
};
const rule$c = {
meta: {
docs: {
description: 'This rule catches usages of events using @Listen decorator.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
const stencil = stencilComponentContext();
return {
...stencil.rules,
'MethodDefinition[kind=method]': (node) => {
if (!stencil.isComponent()) {
return;
}
const listenDec = getDecorator(node, 'Listen');
if (listenDec) {
const [eventName, opts] = parseDecorator(listenDec);
if (typeof eventName === 'string' && opts === undefined) {
const eventName = listenDec.expression.arguments[0].value;
if (PREFER_VDOM_LISTENER.includes(eventName)) {
context.report({
node: listenDec,
message: `Use vDOM listener instead.`
});
}
}
}
}
};
}
};
const PREFER_VDOM_LISTENER = [
'click',
'touchstart',
'touchend',
'touchmove',
'mousedown',
'mouseup',
'mousemove',
'keyup',
'keydown',
'focusin',
'focusout',
'focus',
'blur'
];
const rule$b = {
meta: {
docs: {
description: 'This rule catches Stencil Props marked as private or protected.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem',
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
'PropertyDefinition': (node) => {
if (stencil.isComponent() && getDecorator(node, 'Prop')) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
if (isPrivate(originalNode)) {
context.report({
node: node,
message: `Class properties decorated with @Prop() cannot be private nor protected`
});
}
}
}
};
}
};
const rule$a = {
meta: {
docs: {
description: 'This rule catches Stencil Props marked as non readonly.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'layout',
fixable: 'code'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
return {
...stencil.rules,
'PropertyDefinition': (node) => {
const propDecorator = getDecorator(node, 'Prop');
if (stencil.isComponent() && propDecorator) {
const [opts] = parseDecorator(propDecorator);
if (opts && opts.mutable === true) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const hasReadonly = !!(ts.canHaveModifiers(originalNode) &&
ts.getModifiers(originalNode)?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword));
if (!hasReadonly) {
context.report({
node: node.key,
message: `Class properties decorated with @Prop() should be readonly`,
fix(fixer) {
return fixer.insertTextBefore(node.key, 'readonly ');
}
});
}
}
}
};
}
};
/**
* @fileoverview ESLint rules specific to Stencil JS projects.
* @author Tom Chinery <tom.chinery@addtoevent.co.uk>
*/
const rule$9 = {
meta: {
docs: {
description: 'This rule catches Stencil Prop names that share names of Global HTML Attributes.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
const typeChecker = parserServices.program.getTypeChecker();
return {
...stencil.rules,
'MethodDefinition[kind=method][key.name=render] ReturnStatement': (node) => {
if (!stencil.isComponent()) {
return;
}
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node.argument);
const type = typeChecker.getTypeAtLocation(originalNode);
if (type && type.symbol && type.symbol.escapedName === 'Array') {
context.report({
node: node,
message: `Avoid returning an array in the render() function, use <Host> instead.`
});
}
}
};
}
};
const DECORATORS = ['Prop', 'Method', 'Event'];
const INVALID_TAGS = ['type', 'memberof'];
const rule$8 = {
meta: {
docs: {
description: 'This rule catches Stencil Props and Methods using jsdoc.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'layout'
},
create(context) {
const stencil = stencilComponentContext();
const parserServices = context.sourceCode.parserServices;
function getJSDoc(node) {
if (!stencil.isComponent()) {
return;
}
DECORATORS.forEach((decName) => {
if (getDecorator(node, decName)) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const jsDoc = originalNode.jsDoc;
const isValid = jsDoc && jsDoc.length;
const haveTags = isValid &&
jsDoc.some((jsdoc) => jsdoc.tags && jsdoc.tags.length && jsdoc.tags.some((tag) => INVALID_TAGS.includes(tag.tagName.escapedText.toLowerCase())));
if (!isValid) {
context.report({
node: node,
message: `The @${decName} decorator must be documented.`
});
}
else if (haveTags) {
context.report({
node: node,
message: `The @${decName} decorator have not valid tags (${INVALID_TAGS.join(', ')}).`
});
}
}
});
}
return {
...stencil.rules,
'PropertyDefinition': getJSDoc,
'MethodDefinition[kind=method]': getJSDoc
};
}
};
const rule$7 = {
meta: {
docs: {
description: 'This rule catches required prefix in component tag name.',
category: 'Possible Errors',
recommended: false
},
schema: [
{
type: 'array',
minLength: 1,
additionalProperties: false
}
],
type: 'layout'
},
create(context) {
const stencil = stencilComponentContext();
return {
...stencil.rules,
'ClassDeclaration': (node) => {
const component = getDecorator(node, 'Component');
if (!component) {
return;
}
const [{ tag }] = parseDecorator(component);
const options = context.options[0];
const match = options.some((t) => tag.startsWith(t));
if (!match) {
context.report({
node: node,
message: `The component with tagName ${tag} have not a valid prefix.`
});
}
}
};
}
};
/**
* @fileoverview ESLint rules specific to Stencil JS projects.
* @author Tom Chinery <tom.chinery@addtoevent.co.uk>
*/
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const rule$6 = {
meta: {
docs: {
description: 'This rule catches Stencil Prop names that share names of Global HTML Attributes.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
const stencil = stencilComponentContext();
const checkName = (node) => {
if (!stencil.isComponent()) {
return;
}
const decoratorName = node.expression.callee.name;
if (decoratorName === 'Prop' || decoratorName === 'Method') {
const propName = node.parent.key.name;
if (isReservedMember(propName)) {
context.report({
node: node.parent.key,
message: `The @${decoratorName} name "${propName} conflicts with a key in the HTMLElement prototype. Please choose a different name.`
});
}
if (propName.startsWith('data-')) {
context.report({
node: node.parent.key,
message: 'Avoid using Global HTML Attributes as Prop names.'
});
}
}
};
return {
...stencil.rules,
'PropertyDefinition > Decorator[expression.callee.name=Prop]': checkName,
'MethodDefinition[kind=method] > Decorator[expression.callee.name=Method]': checkName
};
}
};
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
const GLOBAL_ATTRIBUTES = [
'about',
'accessKey',
'autocapitalize',
'autofocus',
'class',
'contenteditable',
'contextmenu',
'dir',
'draggable',
'enterkeyhint',
'hidden',
'id',
'inert',
'inputmode',
'id',
'itemid',
'itemprop',
'itemref',
'itemscope',
'itemtype',
'lang',
'nonce',
'part',
'popover',
'role',
'slot',
'spellcheck',
'style',
'tabindex',
'title',
'translate',
'virtualkeyboardpolicy',
];
const JSX_KEYS = [
'ref',
'key'
];
function getHtmlElementProperties() {
const { window: win } = new JSDOM();
const { document: doc } = win;
const htmlElement = doc.createElement("tester-component"); // creates a custom element base (HTMLElement)
const relevantInterfaces = [win.HTMLElement, win.Element, win.Node, win.EventTarget];
const props = new Set();
let currentInstance = htmlElement;
while (currentInstance && relevantInterfaces.some(relevantInterface => currentInstance instanceof relevantInterface)) {
Object.getOwnPropertyNames(currentInstance).forEach((prop) => props.add(prop));
currentInstance = Object.getPrototypeOf(currentInstance);
}
return Array.from(props);
}
const RESERVED_PUBLIC_MEMBERS = new Set([
...GLOBAL_ATTRIBUTES,
...getHtmlElementProperties(),
...JSX_KEYS
].map(p => p.toLowerCase()));
function isReservedMember(memberName) {
return RESERVED_PUBLIC_MEMBERS.has(memberName.toLowerCase());
}
const rule$5 = {
meta: {
docs: {
description: 'This rule catches modules that expose more than just the Stencil Component itself.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'problem'
},
create(context) {
const parserServices = context.sourceCode.parserServices;
const typeChecker = parserServices.program.getTypeChecker();
return {
'ClassDeclaration': (node) => {
const component = getDecorator(node, 'Component');
if (component) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const nonTypeExports = typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(originalNode.getSourceFile()))
.filter(symbol => (symbol.flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias)) === 0)
.filter(symbol => symbol.name !== originalNode.name.text);
nonTypeExports.forEach(symbol => {
const errorNode = (symbol.valueDeclaration)
? parserServices.tsNodeToESTreeNodeMap.get(symbol.valueDeclaration).id
: parserServices.tsNodeToESTreeNodeMap.get(symbol.declarations?.[0]);
context.report({
node: errorNode,
message: `To allow efficient bundling, modules using @Component() can only have a single export which is the component class itself. Any other exports should be moved to a separate file. For further information check out: https://stenciljs.com/docs/module-bundling`
});
});
}
}
};
}
};
const mutableProps = new Map();
const mutableAssigned = new Set();
const rule$4 = {
meta: {
docs: {
description: 'This rule catches mutable Props that not need to be mutable.',
category: 'Possible Errors',
recommended: true
},
schema: [],
type: 'layout',
},
create(context) {
const stencil = stencilComponentContext();
function getMutable(node) {
if (!stencil.isComponent()) {
return;
}
const parsed = parseDecorator(node);
const mutable = parsed && parsed.length && parsed[0].mutable || false;
if (mutable) {
const varName = node.parent.key.name;
mutableProps.set(varName, node);
}
}
function checkAssigment(node) {
if (!stencil.isComponent()) {
return;
}
const propName = node.left.property.name;
mutableAssigned.add(propName);
}
return {
'ClassDeclaration': stencil.rules.ClassDeclaration,
'PropertyDefinition > Decorator[expression.callee.name=Prop]': getMutable,
'AssignmentExpression[left.object.type=ThisExpression][left.property.type=Identifier]': checkAssigment,
'ClassDeclaration:exit': (node) => {
const isCmp = stencil.isComponent();
stencil.rules["ClassDeclaration:exit"](node);
if (isCmp) {
mutableAssigned.forEach((propName) => mutableProps.delete(propName));
mutableProps.forEach((varNode, varName) => {
context.report({
node: varNode.parent,
message: `@Prop() "${varName}" should not be mutable`,
});
});
mutableAssigned.clear();
mutableProps.clear();
}
}
};
}
};
const rule$3 = {
meta: {
docs: {
description: 'This rule catches function calls at the top level',
category: 'Possible Errors',
recommended: false
},
schema: [
{
type: 'array',
items: {
type: 'string'
},
minLength: 0,
additionalProperties: false
}
],
type: 'suggestion'
},
create(context) {
const shouldSkip = /\b(spec|e2e|test)\./.test(context.getFilename());
const skipFunctions = context.options[0] || DEFAULTS;
if (shouldSkip) {
return {};
}
return {
'CallExpression': (node) => {
if (skipFunctions.includes(node.callee.name)) {
return;
}
if (!isInScope(node)) {
context.report({
node: node,
message: `Call expressions at the top-level should be avoided.`
});
}
}
};
}
};
const isInScope = (n) => {
const type = n.type;
if (type === 'ArrowFunctionExpression' ||
type === 'FunctionDeclaration' ||
type === 'ClassDeclaration' ||
type === 'ExportNamedDeclaration') {
return true;
}
n = n.parent;
if (n) {
return isInScope(n);
}
return false;
};
const DEFAULTS = ['describe', 'test', 'bind', 'createStore'];
/**
* @license
* Copyright 2016 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const OPTION_ALLOW_NULL_UNION = "allow-null-union";
const OPTION_ALLOW_UNDEFINED_UNION = "allow-undefined-union";
const OPTION_ALLOW_STRING = "allow-string";
const OPTION_ALLOW_ENUM = "allow-enum";
const OPTION_ALLOW_NUMBER = "allow-number";
const OPTION_ALLOW_MIX = "allow-mix";
const OPTION_ALLOW_BOOLEAN_OR_UNDEFINED = "allow-boolean-or-undefined";
const OPTION_ALLOW_ANY_RHS = "allow-any-rhs";
const rule$2 = {
meta: {
docs: {
description: `Restricts the types allowed in boolean expressions. By default only booleans are allowed.
The following nodes are checked:
* Arguments to the \`!\`, \`&&\`, and \`||\` operators
* The condition in a conditional expression (\`cond ? x : y\`)
* Conditions for \`if\`, \`for\`, \`while\`, and \`do-while\` statements.`,
category: 'Possible Errors',
recommended: true
},
schema: [{
type: "array",
items: {
type: "string",
enum: [
OPTION_ALLOW_NULL_UNION,
OPTION_ALLOW_UNDEFINED_UNION,
OPTION_ALLOW_STRING,
OPTION_ALLOW_ENUM,
OPTION_ALLOW_NUMBER,
OPTION_ALLOW_BOOLEAN_OR_UNDEFINED,
OPTION_ALLOW_ANY_RHS
],
},
minLength: 0,
maxLength: 5,
}],
type: 'problem'
},
create(context) {
const parserServices = context.sourceCode.parserServices;
const program = parserServices.program;
const rawOptions = context.options[0] || ['allow-null-union', 'allow-undefined-union', 'allow-boolean-or-undefined'];
const options = parseOptions(rawOptions, true);
const checker = program.getTypeChecker();
function walk(sourceFile) {
ts__namespace.forEachChild(sourceFile, function cb(node) {
switch (node.kind) {
case ts__namespace.SyntaxKind.Pref