diff --git a/package-lock.json b/package-lock.json index 0e99d76..823a714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,41 @@ "integrity": "sha512-D1xlXHZpDonVX+VJ28XtcD5xlu8ex6Fc4cQNnrm2wJvlQnbec9RedhCrhQr6kRAE9XWHSec+JPuTmqJ9jC0qsA==", "dev": true }, + "@types/node": { + "version": "10.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.10.3.tgz", + "integrity": "sha512-dWk7F3b0m6uDLHero7tsnpAi9r2RGPQHGbb0/VCv7DPJRMFtk3RonY1/29vfJKnheRMBa7+uF+vunlg/mBGlxg==", + "dev": true + }, + "@types/prop-types": { + "version": "15.5.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.5.tgz", + "integrity": "sha512-mOrlCEdwX3seT3n0AXNt4KNPAZZxcsABUHwBgFXOt+nvFUXkxCAO6UBJHPrDxWEa2KDMil86355fjo8jbZ+K0Q==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react": { + "version": "16.4.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.14.tgz", + "integrity": "sha512-Gh8irag2dbZ2K6vPn+S8+LNrULuG3zlCgJjVUrvuiUK7waw9d9CFk2A/tZFyGhcMDUyO7tznbx1ZasqlAGjHxA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-redux": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-6.0.9.tgz", + "integrity": "sha512-LatgnnZ7bG63SmzEqGMjAIP1bN36iaXpb4G7UW3SqpHyo+OQ97MnMXm9BoNi720r61+PvseyIUJN4el4GVhAAg==", + "dev": true, + "requires": { + "@types/react": "*", + "redux": "^4.0.0" + } + }, "abab": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", @@ -1117,6 +1152,12 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.7.tgz", + "integrity": "sha512-Nt5VDyOTIIV4/nRFswoCKps1R5CD1hkiyjBE9/thNaNZILLEviVw9yWQw15+O+CpNjQKB/uvdcxFFOrSflY3Yw==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -2370,6 +2411,12 @@ } } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", + "dev": true + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -3399,6 +3446,12 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -4051,6 +4104,16 @@ "sisteransi": "^0.1.1" } }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "dev": true, + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -4100,6 +4163,32 @@ } } }, + "react": { + "version": "16.5.2", + "resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz", + "integrity": "sha512-FDCSVd3DjVTmbEAjUNX6FgfAmQ+ypJfHUsqUJOYNCBUp1h8lqmtC+0mXJ+JjsWx4KAVTkk1vKd1hLQPvEviSuw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "schedule": "^0.5.0" + } + }, + "react-redux": { + "version": "5.0.7", + "resolved": "http://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz", + "integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==", + "dev": true, + "requires": { + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.0.0", + "lodash": "^4.17.5", + "lodash-es": "^4.17.5", + "loose-envify": "^1.1.0", + "prop-types": "^15.6.0" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4166,6 +4255,16 @@ "util.promisify": "^1.0.0" } }, + "redux": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz", + "integrity": "sha512-NnnHF0h0WVE/hXyrB6OlX67LYRuaf/rJcbWvnHHEPCF/Xa/AZpwhs/20WyqzQae5x4SD2F9nPObgBh2rxAgLiA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "symbol-observable": "^1.2.0" + } + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -4652,6 +4751,15 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "schedule": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz", + "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==", + "dev": true, + "requires": { + "object-assign": "^4.1.1" + } + }, "semver": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", @@ -5035,6 +5143,12 @@ "has-flag": "^3.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", diff --git a/package.json b/package.json index 4065d9e..b868e8f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "prettier-check": "diffs=$(prettier --list-different '{src,tests,types,examples,docs}/**/*.ts?(x)'); if [ ! -z $diffs ]; then echo \"Run 'npm run prettier'\nThe following files need formatting:\n$diffs\" && exit 1; fi;", "tests": "jest", "test": "npm run lint && npm run tests -- --runInBand --coverage", - "prettier": "prettier --write '{src,tests,types,examples,docs}/**/*.ts?(x)'" + "prettier": "prettier --write '{src,tests,types,examples,docs}/**/*.ts?(x)'", + "test-files": "npm run dist && tslint --project tsconfig.json 'test-files/**/*.@(ts|tsx)'", + "prepublishOnly": "npm run dist" }, "repository": { "type": "git", @@ -37,8 +39,13 @@ }, "devDependencies": { "@types/jest": "^23.3.2", + "@types/node": "^10.10.3", + "@types/react": "^16.4.14", + "@types/react-redux": "^6.0.9", "jest": "^23.6.0", "prettier": "^1.14.2", + "react": "^16.5.2", + "react-redux": "^5.0.7", "ts-jest": "^23.10.1", "tslint": "^5.11.0", "tslint-config-dabapps": "github:dabapps/tslint-config-dabapps#v0.5.1" diff --git a/src/__reference-rule__.ts b/src/__reference-rule__.ts new file mode 100644 index 0000000..b9c5ffc --- /dev/null +++ b/src/__reference-rule__.ts @@ -0,0 +1,21 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +const FAILURE_STRING = 'Import statements are disallowed'; + +// The walker takes care of all the work. +class NoImportsWalker extends Lint.RuleWalker { + public visitImportDeclaration (node: ts.ImportDeclaration) { + // create a failure at the current position + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), FAILURE_STRING)); + + // call the base version of this visitor to actually parse this node + super.visitImportDeclaration(node); + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoImportsWalker(sourceFile, this.getOptions())); + } +} diff --git a/src/index.ts b/src/index.ts index 4bd8367..09f5280 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1 @@ -export const message = 'Hello, World!'; - -export default message; +export = { rulesDirectory: '.' }; diff --git a/src/noPropIntersectionRule.ts b/src/noPropIntersectionRule.ts new file mode 100644 index 0000000..e2aba8a --- /dev/null +++ b/src/noPropIntersectionRule.ts @@ -0,0 +1,55 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +const FAILURE_STRING = 'Props must not be an intersection in Component or PureComponent params'; + +// The walker takes care of all the work. +class NoPropIntersectionWalker extends Lint.RuleWalker { + public visitClassDeclaration (node: ts.ClassDeclaration) { + if (node.heritageClauses && node.heritageClauses.length) { + node.heritageClauses.forEach((heritage) => { + heritage.forEachChild((child) => { + if (child.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + this.checkExpression(child as ts.ExpressionWithTypeArguments); + } + }); + }); + } + + // call the base version of this visitor to actually parse this node + super.visitClassDeclaration(node); + } + + private getExpressionName (node: ts.ExpressionWithTypeArguments) { + if (ts.isIdentifier(node.expression)) { + return (node.expression as ts.Identifier).escapedText; + } else if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + return (node.expression as ts.PropertyAccessExpression).name.escapedText; + } + + return ''; + } + + private checkExpression (node: ts.ExpressionWithTypeArguments) { + const name = this.getExpressionName(node); + + if (name === 'PureComponent' || name === 'Component') { + const { typeArguments } = node; + + if (typeArguments) { + typeArguments.forEach((typeArgument) => { + if (typeArgument.kind === ts.SyntaxKind.IntersectionType) { + // create a failure at the current position + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), FAILURE_STRING)); + } + }); + } + } + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoPropIntersectionWalker(sourceFile, this.getOptions())); + } +} diff --git a/src/noPropUnionRule.ts b/src/noPropUnionRule.ts new file mode 100644 index 0000000..a0a1bdd --- /dev/null +++ b/src/noPropUnionRule.ts @@ -0,0 +1,55 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +const FAILURE_STRING = 'Props must not be an union in Component or PureComponent params'; + +// The walker takes care of all the work. +class NoPropUnionWalker extends Lint.RuleWalker { + public visitClassDeclaration (node: ts.ClassDeclaration) { + if (node.heritageClauses && node.heritageClauses.length) { + node.heritageClauses.forEach((heritage) => { + heritage.forEachChild((child) => { + if (child.kind === ts.SyntaxKind.ExpressionWithTypeArguments) { + this.checkExpression(child as ts.ExpressionWithTypeArguments); + } + }); + }); + } + + // call the base version of this visitor to actually parse this node + super.visitClassDeclaration(node); + } + + private getExpressionName (node: ts.ExpressionWithTypeArguments) { + if (ts.isIdentifier(node.expression)) { + return (node.expression as ts.Identifier).escapedText; + } else if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + return (node.expression as ts.PropertyAccessExpression).name.escapedText; + } + + return ''; + } + + private checkExpression (node: ts.ExpressionWithTypeArguments) { + const name = this.getExpressionName(node); + + if (name === 'PureComponent' || name === 'Component') { + const { typeArguments } = node; + + if (typeArguments) { + typeArguments.forEach((typeArgument) => { + if (typeArgument.kind === ts.SyntaxKind.UnionType) { + // create a failure at the current position + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), FAILURE_STRING)); + } + }); + } + } + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new NoPropUnionWalker(sourceFile, this.getOptions())); + } +} diff --git a/src/requirePropAliasRule.ts b/src/requirePropAliasRule.ts new file mode 100644 index 0000000..1d43913 --- /dev/null +++ b/src/requirePropAliasRule.ts @@ -0,0 +1,21 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +const FAILURE_STRING = 'Props must alias individual prop types (State, Dispatch, Own, etc)'; + +// The walker takes care of all the work. +class RequirePropAliasWalker extends Lint.RuleWalker { + public visitImportDeclaration (node: ts.ImportDeclaration) { + // create a failure at the current position + this.addFailure(this.createFailure(node.getStart(), node.getWidth(), FAILURE_STRING)); + + // call the base version of this visitor to actually parse this node + super.visitImportDeclaration(node); + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new RequirePropAliasWalker(sourceFile, this.getOptions())); + } +} diff --git a/test-files/failure.tsx b/test-files/failure.tsx new file mode 100644 index 0000000..9fab8da --- /dev/null +++ b/test-files/failure.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; + +const { PureComponent } = React; + +interface Props { + foo: 'bar'; +} + +interface DispatchProps { + baz: () => null; +} + +export class MyComponent extends React.Component { + public render () { + return ( +

+ Hello, World! +

+ ); + } +} + +export class MyOtherComponent extends PureComponent<{} | Props> { + public render () { + return
; + } +} + +export class YetAnotherComponent extends PureComponent { + public render () { + return
; + } +} + + +export const mapStateToProps = () => { + return { + foo: 'not-bar' + }; +} + +function mapDispatchToProps () { + return { + baz: () => 'this-is-wrong' + }; +} + +export default connect<{}>(mapStateToProps, mapDispatchToProps)(MyComponent); diff --git a/test-files/tslint.json b/test-files/tslint.json new file mode 100644 index 0000000..23d551e --- /dev/null +++ b/test-files/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "../dist/" + ], + "rules": { + "no-prop-intersection": true, + "no-prop-union": true, + "require-prop-alias": true + } +} diff --git a/tests/index.ts b/tests/index.ts index 4d83912..bca2ad4 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,7 +1,8 @@ -import message from '../src'; +// tslint:disable-next-line:no-var-requires +const index = require('../src'); describe('index.ts', () => { - it('should export hello world', () => { - expect(message).toBe('Hello, World!'); + it('should export a reference to its directory', () => { + expect(index).toEqual({ rulesDirectory: '.' }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 17bcbda..a5cc75e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "./src/", "./examples/", "./tests/", - "./types/" + "./types/", + "./test-files" ] } diff --git a/tslint.json b/tslint.json index 3676fac..7c01a24 100644 --- a/tslint.json +++ b/tslint.json @@ -3,6 +3,7 @@ "tslint-config-dabapps" ], "rules": { - "no-unused-variable": false + "no-unused-variable": false, + "max-classes-per-file": false } }