diff --git a/package.json b/package.json
index ac19600..23291ce 100644
--- a/package.json
+++ b/package.json
@@ -12,31 +12,38 @@
],
"main": "dist/index.js",
"files": [
- "dist"
+ "dist/index.js"
],
"scripts": {
- "build": "tsc -p .",
+ "build": "tsc -p . && rollup -c",
"test": "jest --coverage",
"version": "npm run build",
"release": "np"
},
"dependencies": {
- "@types/eslint": "^6.1.3",
- "@typescript-eslint/parser": "^2.5.0",
"eslint-utils": "^1.4.3",
"tsutils": "^3.17.1"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^2.5.0",
- "eslint": "^6.5.1"
+ "@typescript-eslint/parser": "^2.4.0",
+ "@typescript-eslint/eslint-plugin": "^2.4.0",
+ "eslint-plugin-react": "^7.16.0",
+ "eslint": "^6.0.0",
+ "typescript": "^3.5.0"
},
"devDependencies": {
+ "@types/eslint": "^6.1.3",
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.5.0",
+ "@typescript-eslint/parser": "^2.5.0",
"eslint": "^6.5.1",
+ "eslint-plugin-react": "^7.16.0",
+ "eslint-utils": "^1.4.2",
"jest": "^24.9.0",
"jest-cli": "^24.9.0",
"np": "^5.1.1",
+ "rollup": "^1.24.0",
+ "rollup-plugin-node-resolve": "^5.2.0",
"ts-jest": "^24.1.0",
"typescript": "3.6.4"
},
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..89c47fa
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,22 @@
+import nodeResolve from 'rollup-plugin-node-resolve';
+export default {
+ input: 'dist/build/index.js',
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true
+ })
+ ],
+ treeshake: {
+ pureExternalModules: true
+ },
+ external: [
+ 'eslint',
+ 'typescript',
+ 'tsutils',
+ 'eslint-utils'
+ ],
+ output: {
+ file: 'dist/index.js',
+ format: 'commonjs'
+ }
+};
diff --git a/src/configs/base.ts b/src/configs/base.ts
new file mode 100644
index 0000000..468cf3f
--- /dev/null
+++ b/src/configs/base.ts
@@ -0,0 +1,32 @@
+export default {
+ overrides: [
+ {
+ files: ['*.ts', '*.tsx'],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ 'jsx': true
+ }
+ },
+ plugins: [
+ '@stencil'
+ ],
+ rules: {
+ '@stencil/async-methods': 2,
+ '@stencil/ban-prefix': [2, ['stencil', 'stnl', 'st']],
+ '@stencil/decorators-context': 2,
+ '@stencil/element-type': 2,
+ '@stencil/host-data-deprecated': 2,
+ '@stencil/methods-must-be-public': 2,
+ '@stencil/no-unused-watch': 2,
+ '@stencil/prefer-vdom-listener': 2,
+ '@stencil/props-must-be-public': 2,
+ '@stencil/render-returns-host': 2,
+ '@stencil/reserved-member-names': 2,
+ '@stencil/single-export': 2,
+ }
+ }
+ ]
+};
diff --git a/src/configs/index.ts b/src/configs/index.ts
index 5f7d6b7..27c2e61 100644
--- a/src/configs/index.ts
+++ b/src/configs/index.ts
@@ -1,5 +1,9 @@
+import base from './base';
import recommended from './recommended';
+import strict from './strict';
export default {
- recommended
+ base,
+ recommended,
+ strict
};
diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts
index e0ae51a..7dd2bd8 100644
--- a/src/configs/recommended.ts
+++ b/src/configs/recommended.ts
@@ -1,37 +1,13 @@
export default {
- overrides: [
- {
- parser: '@typescript-eslint/parser',
- files: ['*.ts', '*.tsx'],
- parserOptions: {
- ecmaVersion: 2018,
- sourceType: 'module',
- ecmaFeatures: {
- 'jsx': true
- }
- },
+plugins: [
+ "react"
+ ],
+ extends: [
+ "plugin:@stencil/base",
+ ],
rules: {
'@d0whc3r/stencil/async-methods': 'error',
'@d0whc3r/stencil/ban-prefix': ['error', ['stencil', 'stnl', 'st']],
- // '@d0whc3r/stencil/component-order': [
- // 'error', {
- // order: [
- // 'own-prop',
- // 'element',
- // 'state',
- // 'watched-state',
- // 'prop',
- // 'watched-prop',
- // 'event',
- // 'lifecycle',
- // 'listen',
- // 'method',
- // 'own-method',
- // 'render'
- // ],
- // followingWatch: true,
- // alphabetical: true
- // }],
'@d0whc3r/stencil/decorators-context': 'error',
'@d0whc3r/stencil/decorators-style': [
'error', {
@@ -57,10 +33,36 @@ export default {
'@d0whc3r/stencil/required-jsdoc': 'error',
'@d0whc3r/stencil/reserved-member-names': 'error',
'@d0whc3r/stencil/single-export': 'error',
- '@d0whc3r/stencil/strict-mutable': 'error'
- },
- plugins: [
- '@d0whc3r/stencil'
- ]
+ '@d0whc3r/stencil/strict-mutable': 'error,
+ "react/jsx-no-bind": [1, {
+ "ignoreRefs": true
}]
+ }
};
+
+/*
+ rules: {
+ '@stencil/strict-boolean-conditions': 2,
+ '@stencil/ban-exported-const-enums': 2,
+ '@stencil/ban-side-effects': 2,
+ '@stencil/strict-mutable': 2,
+ '@stencil/decorators-style': [
+ 'error', {
+ prop: 'inline',
+ state: 'inline',
+ element: 'inline',
+ event: 'inline',
+ method: 'multiline',
+ watch: 'multiline',
+ listen: 'multiline'
+ }
+ ],
+ '@stencil/own-methods-must-be-private': 1,
+ '@stencil/own-props-must-be-private': 1,
+ '@stencil/dependency-suggestions': 1,
+ '@stencil/required-jsdoc': 1,
+ "react/jsx-no-bind": [1, {
+ "ignoreRefs": true
+ }]
+ }
+*/
\ No newline at end of file
diff --git a/src/configs/strict.ts b/src/configs/strict.ts
new file mode 100644
index 0000000..f0a00bd
--- /dev/null
+++ b/src/configs/strict.ts
@@ -0,0 +1,58 @@
+export default {
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@stencil/recommended",
+ ],
+ rules: {
+ // Resets
+ "@typescript-eslint/camelcase": 0,
+ "@typescript-eslint/explicit-function-return-type": 0,
+ "@typescript-eslint/ban-ts-ignore": 0,
+ "@typescript-eslint/no-this-alias": 0,
+ "@typescript-eslint/no-non-null-assertion": 0,
+ "@typescript-eslint/no-unused-vars": 0,
+ "@typescript-eslint/no-empty-interface": 0,
+ "@typescript-eslint/no-use-before-define": 0,
+ "@typescript-eslint/no-explicit-any": 0,
+ "no-constant-condition": 0,
+
+
+ // Best practices
+ "no-shadow": 2,
+ "require-atomic-updates": 2,
+ "no-var": 2,
+ "prefer-object-spread": 2,
+ "no-nested-ternary": 2,
+ "no-duplicate-imports": 2,
+
+ // General formatting
+ "indent": [2, 2],
+ "no-trailing-spaces": 2,
+ "curly": [2, "all"],
+ "comma-spacing": 2,
+ "comma-style": 2,
+ "computed-property-spacing": 2,
+ "comma-dangle": [2, "always-multiline"],
+ "func-style": [2, "expression", { "allowArrowFunctions": true }],
+ "multiline-ternary": [2, "always-multiline"],
+ "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
+ "linebreak-style": 2,
+ "space-in-parens": 2,
+ "@typescript-eslint/semi": 2,
+ "@typescript-eslint/brace-style": 2,
+ "@typescript-eslint/func-call-spacing": 2,
+
+ // JSX formatting
+ "react/jsx-closing-tag-location": 2,
+ "react/jsx-curly-newline": [2, "never"],
+ "react/jsx-closing-bracket-location": 2,
+ "react/jsx-curly-spacing": [2, {"when": "never", "children": true}],
+ "react/jsx-boolean-value": [2, "never"],
+ "react/jsx-child-element-spacing": 2,
+ "react/jsx-indent-props": [2, "first"],
+ "react/jsx-props-no-multi-spaces": 2,
+ "react/jsx-equals-spacing": [2, "never"],
+ }
+};
diff --git a/src/index.ts b/src/index.ts
index 7ead455..7146c94 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,7 +6,7 @@
import rules from './rules';
import configs from './configs';
-export = {
+export {
rules,
configs
};
diff --git a/src/rules/async-methods.ts b/src/rules/async-methods.ts
index eda28b6..dbc94fb 100644
--- a/src/rules/async-methods.ts
+++ b/src/rules/async-methods.ts
@@ -1,7 +1,7 @@
import { Rule } from 'eslint';
import ts from 'typescript';
import { stencilComponentContext } from '../utils';
-import * as tsutils from 'tsutils';
+import { isThenableType } from 'tsutils';
const rule: Rule.RuleModule = {
meta: {
@@ -30,7 +30,7 @@ const rule: Rule.RuleModule = {
const method = parserServices.esTreeNodeToTSNodeMap.get(node);
const signature = typeChecker.getSignatureFromDeclaration(method);
const returnType = typeChecker.getReturnTypeOfSignature(signature!);
- if (!tsutils.isThenableType(typeChecker, method, returnType)) {
+ if (!isThenableType(typeChecker, method, returnType)) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node) as ts.Node;
const text = String(originalNode.getFullText());
context.report({
diff --git a/src/rules/ban-exported-const-enums.ts b/src/rules/ban-exported-const-enums.ts
new file mode 100644
index 0000000..4f9c327
--- /dev/null
+++ b/src/rules/ban-exported-const-enums.ts
@@ -0,0 +1,26 @@
+import { Rule } from 'eslint';
+
+const rule: Rule.RuleModule = {
+ meta: {
+ docs: {
+ description: 'This rule catches exports of const enums',
+ category: 'Possible Errors',
+ recommended: true
+ },
+ schema: [],
+ type: 'problem'
+ },
+
+ create(context): Rule.RuleListener {
+ return {
+ 'ExportNamedDeclaration > TSEnumDeclaration[const]': (node: any) => {
+ context.report({
+ node: node,
+ message: `Exported const enums are not allowed`
+ });
+ }
+ };
+ }
+};
+
+export default rule;
diff --git a/src/rules/ban-side-effects.ts b/src/rules/ban-side-effects.ts
new file mode 100644
index 0000000..0e756c5
--- /dev/null
+++ b/src/rules/ban-side-effects.ts
@@ -0,0 +1,65 @@
+import { Rule } from 'eslint';
+
+const rule: Rule.RuleModule = {
+ 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): Rule.RuleListener {
+ const shouldSkip = /\b(spec|e2e|test)\./.test(context.getFilename());
+ const skipFunctions = context.options[0] || DEFAULTS;
+ if (shouldSkip) {
+ return {};
+ }
+ return {
+ 'CallExpression': (node: any) => {
+ 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: any): boolean => {
+ 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'];
+
+export default rule;
+
diff --git a/src/rules/decorators-style.ts b/src/rules/decorators-style.ts
index a055050..8e3f851 100644
--- a/src/rules/decorators-style.ts
+++ b/src/rules/decorators-style.ts
@@ -117,7 +117,7 @@ const rule: Rule.RuleModule = {
return {
...stencil.rules,
'ClassProperty': getStyle,
- 'MethodDefinition': getStyle
+ 'MethodDefinition[kind=method]': getStyle
};
}
};
diff --git a/src/rules/dependency-suggestions.ts b/src/rules/dependency-suggestions.ts
new file mode 100644
index 0000000..0c25859
--- /dev/null
+++ b/src/rules/dependency-suggestions.ts
@@ -0,0 +1,38 @@
+import { Rule } from 'eslint';
+import ts from 'typescript';
+import { stencilComponentContext } from '../utils';
+import * as tsutils from 'tsutils';
+
+const rule: Rule.RuleModule = {
+ meta: {
+ docs: {
+ description: 'This rule can provide suggestions about dependencies in stencil apps',
+ recommended: true
+ },
+ schema: [],
+ type: 'suggestion',
+ },
+
+ create(context): Rule.RuleListener {
+ return {
+ 'ImportDeclaration': (node: any) => {
+ const importName = node.source.value;
+ const message = SUGGESTIONS[importName];
+ if (message) {
+ context.report({
+ node,
+ message
+ });
+ }
+ }
+ };
+ }
+};
+
+const SUGGESTIONS: {[importName: string]: string} = {
+ 'classnames': `Stencil can already render conditional classes:
+
`,
+ 'lodash': `"lodash" will bloat your build, use "lodash-es" instead: https://www.npmjs.com/package/lodash-es`
+}
+
+export default rule;
diff --git a/src/rules/element-type.ts b/src/rules/element-type.ts
index e0494b4..15e9069 100644
--- a/src/rules/element-type.ts
+++ b/src/rules/element-type.ts
@@ -34,16 +34,13 @@ const rule: Rule.RuleModule = {
const component = getDecorator(node.parent.parent.parent, 'Component');
const [{ tag }] = parseDecorator(component);
const parsedTag = `HTML${parseTag(tag)}Element`;
+
if (tagType !== parsedTag) {
- const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node.parent);
- const text = originalNode.getFullText();
- const type = originalNode.type.typeName.escapedText;
context.report({
- node: node.parent,
+ node: node.parent.typeAnnotation,
message: `@Element type is not matching tag for component (${parsedTag})`,
fix(fixer) {
- const result = text.replace(`: ${type}`, `: ${parsedTag}`);
- return fixer.replaceText(node.parent, result);
+ return fixer.replaceText(node.parent.typeAnnotation.typeAnnotation, parsedTag);
}
});
}
diff --git a/src/rules/index.ts b/src/rules/index.ts
index cb03e75..d1c949f 100644
--- a/src/rules/index.ts
+++ b/src/rules/index.ts
@@ -19,8 +19,16 @@ import requiredPrefix from './required-prefix';
import reservedMemberNames from './reserved-member-names';
import singleExport from './single-export';
import strictMutable from './strict-mutable';
+import banSideEffects from './ban-side-effects';
+import strictBooleanConditions from './strict-boolean-conditions';
+import banExportedConstEnums from './ban-exported-const-enums';
+import dependencySuggestions from './dependency-suggestions';
export default {
+ 'ban-side-effects': banSideEffects,
+ 'ban-exported-const-enums': banExportedConstEnums,
+ 'dependency-suggestions': dependencySuggestions,
+ 'strict-boolean-conditions': strictBooleanConditions,
'async-methods': asyncMethods,
'ban-prefix': banPrefix,
'class-pattern': classPattern,
diff --git a/src/rules/methods-must-be-public.ts b/src/rules/methods-must-be-public.ts
index b4f949c..08ddf1e 100644
--- a/src/rules/methods-must-be-public.ts
+++ b/src/rules/methods-must-be-public.ts
@@ -19,7 +19,7 @@ const rule: Rule.RuleModule = {
const parserServices = context.parserServices;
return {
...stencil.rules,
- 'MethodDefinition': (node: any) => {
+ 'MethodDefinition[kind=method]': (node: any) => {
if (stencil.isComponent() && getDecorator(node, 'Method')) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node) as ts.Node;
if (isPrivate(originalNode)) {
diff --git a/src/rules/no-unused-watch.ts b/src/rules/no-unused-watch.ts
index c12a215..d9c5f93 100644
--- a/src/rules/no-unused-watch.ts
+++ b/src/rules/no-unused-watch.ts
@@ -47,7 +47,7 @@ const rule: Rule.RuleModule = {
'ClassDeclaration': stencil.rules.ClassDeclaration,
'ClassProperty > Decorator[expression.callee.name=Prop]': getVars,
'ClassProperty > Decorator[expression.callee.name=State]': getVars,
- 'MethodDefinition > Decorator[expression.callee.name=Watch]': checkWatch,
+ 'MethodDefinition[kind=method] > Decorator[expression.callee.name=Watch]': checkWatch,
'ClassDeclaration:exit': (node: any) => {
if (!stencil.isComponent()) {
return;
diff --git a/src/rules/own-methods-must-be-private.ts b/src/rules/own-methods-must-be-private.ts
index 82bd2e3..f8b74d1 100644
--- a/src/rules/own-methods-must-be-private.ts
+++ b/src/rules/own-methods-must-be-private.ts
@@ -20,7 +20,7 @@ const rule: Rule.RuleModule = {
const parserServices = context.parserServices;
return {
...stencil.rules,
- 'MethodDefinition': (node: any) => {
+ 'MethodDefinition[kind=method]': (node: any) => {
if (!stencil.isComponent()) {
return;
}
diff --git a/src/rules/prefer-vdom-listener.ts b/src/rules/prefer-vdom-listener.ts
index 70c3bf8..b953bbb 100644
--- a/src/rules/prefer-vdom-listener.ts
+++ b/src/rules/prefer-vdom-listener.ts
@@ -16,7 +16,7 @@ const rule: Rule.RuleModule = {
const stencil = stencilComponentContext();
return {
...stencil.rules,
- 'MethodDefinition': (node: any) => {
+ 'MethodDefinition[kind=method]': (node: any) => {
if (!stencil.isComponent()) {
return;
}
diff --git a/src/rules/render-returns-host.ts b/src/rules/render-returns-host.ts
index e061264..e8212fb 100644
--- a/src/rules/render-returns-host.ts
+++ b/src/rules/render-returns-host.ts
@@ -33,7 +33,7 @@ const rule: Rule.RuleModule = {
return {
...stencil.rules,
- 'MethodDefinition[key.name=render] ReturnStatement': (node: any) => {
+ 'MethodDefinition[kind=method][key.name=render] ReturnStatement': (node: any) => {
if (!stencil.isComponent()) {
return;
}
diff --git a/src/rules/required-jsdoc.ts b/src/rules/required-jsdoc.ts
index a3e3d23..359400d 100644
--- a/src/rules/required-jsdoc.ts
+++ b/src/rules/required-jsdoc.ts
@@ -51,7 +51,7 @@ const rule: Rule.RuleModule = {
return {
...stencil.rules,
'ClassProperty': getJSDoc,
- 'MethodDefinition': getJSDoc
+ 'MethodDefinition[kind=method]': getJSDoc
};
}
};
diff --git a/src/rules/required-prefix.ts b/src/rules/required-prefix.ts
index 5cd4123..a04cca4 100644
--- a/src/rules/required-prefix.ts
+++ b/src/rules/required-prefix.ts
@@ -29,10 +29,6 @@ const rule: Rule.RuleModule = {
return;
}
const [{ tag }] = parseDecorator(component);
- if (!tag) {
- console.warn('[required-prefix] No tag detected for component');
- return;
- }
const options = context.options[0];
const match = options.some((t: string) => tag.startsWith(t));
diff --git a/src/rules/reserved-member-names.ts b/src/rules/reserved-member-names.ts
index 651dc81..ce342c9 100644
--- a/src/rules/reserved-member-names.ts
+++ b/src/rules/reserved-member-names.ts
@@ -52,7 +52,7 @@ const rule: Rule.RuleModule = {
return {
...stencil.rules,
'ClassProperty > Decorator[expression.callee.name=Prop]': checkName,
- 'MethodDefinition > Decorator[expression.callee.name=Method]': checkName
+ 'MethodDefinition[kind=method] > Decorator[expression.callee.name=Method]': checkName
};
}
};
diff --git a/src/rules/strict-boolean-conditions.ts b/src/rules/strict-boolean-conditions.ts
new file mode 100644
index 0000000..dd5c9d8
--- /dev/null
+++ b/src/rules/strict-boolean-conditions.ts
@@ -0,0 +1,448 @@
+/**
+ * @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.
+ */
+import * as ts from "typescript";
+
+import { Rule } from 'eslint';
+
+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: Rule.RuleModule = {
+ 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): Rule.RuleListener {
+ const parserServices = context.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() as ts.TypeChecker;
+
+ function walk(sourceFile: ts.SourceFile): void {
+ ts.forEachChild(sourceFile, function cb(node: ts.Node): void {
+ switch (node.kind) {
+ case ts.SyntaxKind.PrefixUnaryExpression: {
+ const {
+ operator,
+ operand
+ } = node as ts.PrefixUnaryExpression;
+ if (operator === ts.SyntaxKind.ExclamationToken) {
+ checkExpression(operand, node as ts.PrefixUnaryExpression);
+ }
+ break;
+ }
+
+ case ts.SyntaxKind.IfStatement:
+ case ts.SyntaxKind.WhileStatement:
+ case ts.SyntaxKind.DoStatement: {
+ const c = node as ts.IfStatement | ts.WhileStatement | ts.DoStatement;
+ // If it's a boolean binary expression, we'll check it when recursing.
+ checkExpression(c.expression, c);
+ break;
+ }
+
+ case ts.SyntaxKind.ConditionalExpression:
+ checkExpression((node as ts.ConditionalExpression).condition, node as ts.ConditionalExpression);
+ break;
+
+ case ts.SyntaxKind.ForStatement: {
+ const {
+ condition
+ } = node as ts.ForStatement;
+ if (condition !== undefined) {
+ checkExpression(condition, node as ts.ForStatement);
+ }
+ }
+ }
+
+ return ts.forEachChild(node, cb);
+ });
+
+ function checkExpression(node: ts.Expression, location: Location): void {
+ const type = checker.getTypeAtLocation(node);
+ const failure = getTypeFailure(type, options);
+ if (failure !== undefined) {
+ if (failure === TypeFailure.AlwaysTruthy &&
+ !options.strictNullChecks &&
+ (options.allowNullUnion || options.allowUndefinedUnion)) {
+ // OK; It might be null/undefined.
+ return;
+ }
+ const originalNode = parserServices.tsNodeToESTreeNodeMap.get(node);
+ context.report({
+ node: originalNode,
+ message: showFailure(location, failure, isUnionType(type), options),
+ })
+ }
+ }
+ }
+
+ return {
+ 'Program': (node: any) => {
+ const sourceFile = parserServices.esTreeNodeToTSNodeMap.get(node);
+ walk(sourceFile);
+ }
+ };
+ }
+};
+
+
+
+interface Options {
+ strictNullChecks: boolean;
+ allowNullUnion: boolean;
+ allowUndefinedUnion: boolean;
+ allowString: boolean;
+ allowEnum: boolean;
+ allowNumber: boolean;
+ allowMix: boolean;
+ allowBooleanOrUndefined: boolean;
+ allowAnyRhs: boolean;
+}
+
+function parseOptions(ruleArguments: string[], strictNullChecks: boolean): Options {
+ 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: string): boolean {
+ return ruleArguments.indexOf(name) !== -1;
+ }
+}
+
+function getTypeFailure(type: ts.Type, options: Options): TypeFailure | undefined {
+ if (isUnionType(type)) {
+ return handleUnion(type, options);
+ }
+
+ const kind = getKind(type);
+ const failure = failureForKind(kind, /*isInUnion*/ false, options);
+ if (failure !== undefined) {
+ return failure;
+ }
+
+ switch (triState(kind)) {
+ case true:
+ // Allow 'any'. Allow 'true' itself, but not any other always-truthy type.
+ // tslint:disable-next-line no-bitwise
+ return isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.BooleanLiteral) ? undefined : TypeFailure.AlwaysTruthy;
+ case false:
+ // Allow 'false' itself, but not any other always-falsy type
+ return isTypeFlagSet(type, ts.TypeFlags.BooleanLiteral) ? undefined : TypeFailure.AlwaysFalsy;
+ case undefined:
+ return undefined;
+ }
+}
+
+function isBooleanUndefined(type: ts.UnionType): boolean | undefined {
+ let isTruthy = false;
+ for (const ty of type.types) {
+ if (isTypeFlagSet(ty, ts.TypeFlags.Boolean)) {
+ isTruthy = true;
+ } else if (isTypeFlagSet(ty, ts.TypeFlags.BooleanLiteral)) {
+ isTruthy = isTruthy || (ty as ts.IntrinsicType).intrinsicName === "true";
+ } else if (!isTypeFlagSet(ty, ts.TypeFlags.Void | ts.TypeFlags.Undefined)) { // tslint:disable-line:no-bitwise
+ return undefined;
+ }
+ }
+ return isTruthy;
+}
+
+function handleUnion(type: ts.UnionType, options: Options): TypeFailure | undefined {
+ if (options.allowBooleanOrUndefined) {
+ switch (isBooleanUndefined(type)) {
+ case true:
+ return undefined;
+ case false:
+ return TypeFailure.AlwaysFalsy;
+ }
+ }
+
+ for (const ty of type.types) {
+ const kind = getKind(ty);
+ const failure = failureForKind(kind, /*isInUnion*/ true, options);
+ if (failure !== undefined) {
+ return failure;
+ }
+ }
+ return undefined;
+}
+
+/** Fails if a kind of falsiness is not allowed. */
+function failureForKind(kind: TypeKind, isInUnion: boolean, options: Options): TypeFailure | undefined {
+ switch (kind) {
+ case TypeKind.String:
+ case TypeKind.FalseStringLiteral:
+ return options.allowString ? undefined : TypeFailure.String;
+ case TypeKind.Number:
+ case TypeKind.FalseNumberLiteral:
+ return options.allowNumber ? undefined : TypeFailure.Number;
+ case TypeKind.Enum:
+ return options.allowEnum ? undefined : TypeFailure.Enum;
+ case TypeKind.Promise:
+ return TypeFailure.Promise;
+ case TypeKind.Null:
+ return isInUnion && !options.allowNullUnion ? TypeFailure.Null : undefined;
+ case TypeKind.Undefined:
+ return isInUnion && !options.allowUndefinedUnion ? TypeFailure.Undefined : undefined;
+ default:
+ return undefined;
+ }
+}
+
+export type Location = |
+ ts.PrefixUnaryExpression |
+ ts.IfStatement |
+ ts.WhileStatement |
+ ts.DoStatement |
+ ts.ForStatement |
+ ts.ConditionalExpression |
+ ts.BinaryExpression;
+
+export const enum TypeFailure {
+ AlwaysTruthy,
+ AlwaysFalsy,
+ String,
+ Number,
+ Null,
+ Undefined,
+ Enum,
+ Mixes,
+ Promise
+}
+
+const enum TypeKind {
+ String,
+ FalseStringLiteral,
+ Number,
+ FalseNumberLiteral,
+ Boolean,
+ FalseBooleanLiteral,
+ Null,
+ Undefined,
+ Enum,
+ AlwaysTruthy,
+ Promise
+}
+
+/** Divides a type into always true, always false, or unknown. */
+function triState(kind: TypeKind): boolean | undefined {
+ switch (kind) {
+ case TypeKind.String:
+ case TypeKind.Number:
+ case TypeKind.Boolean:
+ case TypeKind.Enum:
+ return undefined;
+
+ case TypeKind.Null:
+ case TypeKind.Undefined:
+ case TypeKind.FalseNumberLiteral:
+ case TypeKind.FalseStringLiteral:
+ case TypeKind.FalseBooleanLiteral:
+ return false;
+
+ case TypeKind.AlwaysTruthy:
+ case TypeKind.Promise:
+ return true;
+ }
+}
+
+function getKind(type: ts.Type): TypeKind {
+ return is(ts.TypeFlags.StringLike) ? TypeKind.String :
+ is(ts.TypeFlags.NumberLike) ? TypeKind.Number :
+ is(ts.TypeFlags.Boolean) ? TypeKind.Boolean :
+ isObject('Promise') ? TypeKind.Promise :
+ is(ts.TypeFlags.Null) ? TypeKind.Null :
+ is(ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? TypeKind.Undefined // tslint:disable-line:no-bitwise
+ :
+ is(ts.TypeFlags.EnumLike) ? TypeKind.Enum :
+ is(ts.TypeFlags.BooleanLiteral) ?
+ ((type as ts.IntrinsicType).intrinsicName === "true" ? TypeKind.AlwaysTruthy : TypeKind.FalseBooleanLiteral) :
+ TypeKind.AlwaysTruthy;
+
+ function is(flags: ts.TypeFlags) {
+ return isTypeFlagSet(type, flags);
+ }
+
+ function isObject(name: string) {
+ const symbol = type.getSymbol();
+ return (symbol && symbol.getName() === name)
+ }
+}
+
+
+function binaryBooleanExpressionKind(node: ts.BinaryExpression): "&&" | "||" | undefined {
+ switch (node.operatorToken.kind) {
+ case ts.SyntaxKind.AmpersandAmpersandToken:
+ return "&&";
+ case ts.SyntaxKind.BarBarToken:
+ return "||";
+ default:
+ return undefined;
+ }
+}
+
+function stringOr(parts: string[]): string {
+ 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: ts.Type): type is ts.UnionType {
+ return isTypeFlagSet(type, ts.TypeFlags.Union) && !isTypeFlagSet(type, ts.TypeFlags.Enum);
+}
+
+function showLocation(n: Location): string {
+ switch (n.kind) {
+ case ts.SyntaxKind.PrefixUnaryExpression:
+ return "operand for the '!' operator";
+ case ts.SyntaxKind.ConditionalExpression:
+ return "condition";
+ case ts.SyntaxKind.ForStatement:
+ return "'for' condition";
+ case ts.SyntaxKind.IfStatement:
+ return "'if' condition";
+ case ts.SyntaxKind.WhileStatement:
+ return "'while' condition";
+ case ts.SyntaxKind.DoStatement:
+ return "'do-while' condition";
+ case ts.SyntaxKind.BinaryExpression:
+ return `operand for the '${binaryBooleanExpressionKind(n)}' operator`;
+ }
+}
+
+function showFailure(location: Location, ty: TypeFailure, unionType: boolean, options: Options): string {
+ 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: Options): string[] {
+ 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: TypeFailure, unionType: boolean, strictNullChecks: boolean) {
+ const is = unionType ? "could be" : "is";
+ switch (ty) {
+ case TypeFailure.AlwaysTruthy:
+ return strictNullChecks ?
+ "is always truthy" :
+ "is always truthy. It may be null/undefined, but neither " +
+ `'${OPTION_ALLOW_NULL_UNION}' nor '${OPTION_ALLOW_UNDEFINED_UNION}' is set`;
+ case TypeFailure.AlwaysFalsy:
+ return "is always falsy";
+ case TypeFailure.String:
+ return `${is} a string`;
+ case TypeFailure.Number:
+ return `${is} a number`;
+ case TypeFailure.Null:
+ return `${is} null`;
+ case TypeFailure.Undefined:
+ return `${is} undefined`;
+ case TypeFailure.Enum:
+ return `${is} an enum`;
+ case TypeFailure.Promise:
+ return "promise handled as boolean expression";
+ case TypeFailure.Mixes:
+ return "unions more than one truthy/falsy type";
+ }
+}
+
+function isTypeFlagSet(obj: any, flag: any) {
+ return (obj.flags & flag) !== 0;
+}
+
+declare module "typescript" {
+ // No other way to distinguish boolean literal true from boolean literal false
+ export interface IntrinsicType extends ts.Type {
+ intrinsicName: string;
+ }
+}
+
+export default rule;
\ No newline at end of file
diff --git a/src/rules/strict-mutable.ts b/src/rules/strict-mutable.ts
index 338c1ac..1772fa9 100644
--- a/src/rules/strict-mutable.ts
+++ b/src/rules/strict-mutable.ts
@@ -35,18 +35,22 @@ const rule: Rule.RuleModule = {
const parserServices = context.parserServices;
function getMutable(node: any) {
- if (stencil.isComponent()) {
- 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);
- }
+ 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 removeVar(name: any) {
- if (name && name.escapedText) {
+ if (!name) {
+ return;
+ }
+ if (name.escapedText) {
mutableProps.delete(name.escapedText);
}
}
@@ -59,38 +63,32 @@ const rule: Rule.RuleModule = {
}
function checkStatement(st: any) {
- if (st) {
- const { expression, thenStatement, elseStatement } = st;
- [...getArray(thenStatement), ...getArray(elseStatement), expression].filter((ex) => !!ex)
- .forEach(checkExpression);
+ if (!st) {
+ return;
}
+ const { expression, thenStatement, elseStatement } = st;
+ [...getArray(thenStatement), ...getArray(elseStatement), expression].filter((ex) => !!ex).forEach(checkExpression);
}
function checkExpression(expr: any) {
- if (expr) {
- const { expression, left, openingElement, body, nextContainer, statements, thenStatement } = expr;
- const args = expr.arguments;
- if (left && left.name && expr.operatorToken && ASSIGN_TOKENS.includes(expr.operatorToken.kind)) {
- removeVar(left.name);
- }
- if (openingElement) {
- getArray(openingElement.attributes).forEach(checkExpression);
- }
- [...getArray(thenStatement), expression, body, nextContainer, ...getArray(args)].filter((ex) => !!ex)
- .forEach(checkExpression);
- if (statements) {
- statements.forEach(checkStatement);
- }
+ if (!expr) {
+ return;
+ }
+ const { expression, left, openingElement, body, nextContainer, statements, thenStatement } = expr;
+ if (left && left.name && expr.operatorToken && ASSIGN_TOKENS.includes(expr.operatorToken.kind)) {
+ removeVar(left.name);
+ }
+ if (openingElement) {
+ getArray(openingElement.attributes).forEach(checkExpression);
+ }
+ [...getArray(thenStatement), expression, body, nextContainer].filter((ex) => !!ex).forEach(checkExpression);
+ if (statements) {
+ statements.forEach(checkStatement);
}
}
- return {
- 'ClassDeclaration': stencil.rules.ClassDeclaration,
- 'ClassProperty > Decorator[expression.callee.name=Prop]': getMutable,
- 'MethodDefinition': (node: any) => {
- if (!stencil.isComponent()) {
- return;
- }
+ function checkMethod(node: any) {
+ if (stencil.isComponent()) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const statements = originalNode.body.statements;
if (statements && statements.length) {
@@ -98,34 +96,22 @@ const rule: Rule.RuleModule = {
checkStatement(st);
});
}
- },
+ }
+ }
+
+ return {
+ 'ClassDeclaration': stencil.rules.ClassDeclaration,
+ 'ClassProperty > Decorator[expression.callee.name=Prop]': getMutable,
+ 'MethodDefinition, ArrowFunctionExpression': checkMethod,
'ClassDeclaration:exit': (node: any) => {
if (!stencil.isComponent()) {
return;
}
stencil.rules['ClassDeclaration:exit'](node);
mutableProps.forEach((varNode, varName) => {
- const originalNode = parserServices.esTreeNodeToTSNodeMap.get(varNode.parent);
- const text = originalNode.getFullText();
- const parsed = parseDecorator(varNode);
context.report({
node: varNode.parent,
message: `@Prop() "${varName}" should not be mutable`,
- fix(fixer) {
- const options = parsed && parsed.length && parsed[0] || {};
- delete options.mutable;
- let opts = '';
- if (options && Object.keys(options).length) {
- opts = Object.keys(options).map((key) => {
- const value = options[key];
- const val = typeof value === 'string' ? `'${value}'` : value;
- return `${key}: ${val}`;
- }).join(', ');
- opts = `{ ${opts} }`;
- }
- const result = text.replace(/@Prop\((.*)?\)/, `@Prop(${opts})`);
- return fixer.replaceText(varNode, result);
- }
});
});
mutableProps.clear();
diff --git a/src/utils.ts b/src/utils.ts
index 3af4d12..8bb1850 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -32,7 +32,7 @@ export function parseDecorator(decorator: any) {
}
export function decoratorName(dec: any): string {
- return dec && dec.expression && dec.expression.callee.name;
+ return dec && dec.expression && dec.expression.callee && dec.expression.callee.name;
}
export function isDecoratorNamed(propName: string) {
diff --git a/tests/lib/rules/element-type/element-type.spec.ts b/tests/lib/rules/element-type/element-type.spec.ts
index 465cb86..f1b6203 100644
--- a/tests/lib/rules/element-type/element-type.spec.ts
+++ b/tests/lib/rules/element-type/element-type.spec.ts
@@ -20,7 +20,7 @@ describe('stencil rules', () => {
{
code: fs.readFileSync(files.wrong, 'utf8'),
filename: files.wrong,
- errors: 3
+ errors: 1
}
]
});
diff --git a/tests/lib/rules/element-type/element-type.wrong.tsx b/tests/lib/rules/element-type/element-type.wrong.tsx
index fdffab4..f3b97b4 100644
--- a/tests/lib/rules/element-type/element-type.wrong.tsx
+++ b/tests/lib/rules/element-type/element-type.wrong.tsx
@@ -1,9 +1,8 @@
@Component({ tag: 'sample-tag' })
export class TheSampleTag {
- @Element() private theElement!: HTMLElement;
-
- @Element() private readonly theElement!: HTMLElement;
-
+ /**
+ * Element: is the element
+ */
@Element() theElement!: HTMLElement;
render() {
diff --git a/tests/lib/rules/strict-mutable/strict-mutable.good.tsx b/tests/lib/rules/strict-mutable/strict-mutable.good.tsx
index 8b8c1b8..8ae98f1 100644
--- a/tests/lib/rules/strict-mutable/strict-mutable.good.tsx
+++ b/tests/lib/rules/strict-mutable/strict-mutable.good.tsx
@@ -7,7 +7,6 @@ export class SampleTag {
@Prop({ mutable: true }) mutableInJsx?: boolean;
@Prop({ mutable: true }) mutableInJsx2?: boolean;
@Prop({ mutable: true }) testMutableReturn?: boolean;
- @Prop({ mutable: true }) mutableInArrayFn?: boolean;
private internalMethod() {
const test = 'hi';
@@ -18,9 +17,6 @@ export class SampleTag {
}
private onClick(e: Event) {
- this.eventHandler((variable) => {
- this.mutableInArrayFn = variable;
- });
e.preventDefault();
if (!this.testNotMutable) {
this.testMutable = true;
diff --git a/tsconfig.json b/tsconfig.json
index ec3d8d5..e2c2260 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,16 +3,15 @@
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
- "declaration": true,
- "declarationMap": true,
+ "declaration": false,
"esModuleInterop": true,
"experimentalDecorators": true,
- "module": "commonjs",
+ "module": "esnext",
"moduleResolution": "node",
"strict": true,
"target": "es2017",
- "outDir": "dist",
+ "outDir": "dist/build",
"lib": ["es2017"]
},
- "include": ["src"]
+ "files": ["src/index.ts"],
}