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