├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── rxjs-operator-counter ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── crawler.ts └── rules │ └── operatorCounterRule.ts ├── tests ├── __snapshots__ │ └── crawler.spec.ts.snap ├── crawler.spec.ts └── fixtures │ ├── flamingo.ts │ ├── pangolin.ts │ └── tsconfig.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | job_defaults: &job_defaults 4 | docker: 5 | - image: circleci/node:latest 6 | working_directory: ~/project/repo 7 | 8 | cache_key: &cache_key rxjs-operator-counter-deps-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} 9 | dist_key: &dist_key rxjs-operator-counter-dist-{{ .Revision }} 10 | 11 | jobs: 12 | install: 13 | <<: *job_defaults 14 | steps: 15 | - checkout 16 | - restore_cache: 17 | key: *cache_key 18 | - run: 19 | name: install-dependencies 20 | command: yarn 21 | - save_cache: 22 | key: *cache_key 23 | paths: 24 | - node_modules 25 | 26 | build: 27 | <<: *job_defaults 28 | steps: 29 | - checkout 30 | - restore_cache: 31 | key: *cache_key 32 | - run: 33 | name: build 34 | command: yarn build 35 | - save_cache: 36 | key: *dist_key 37 | paths: 38 | - dist 39 | 40 | release: 41 | <<: *job_defaults 42 | steps: 43 | - checkout 44 | - restore_cache: 45 | key: *cache_key 46 | - restore_cache: 47 | key: *dist_key 48 | - run: 49 | name: release 50 | command: npx semantic-release || true 51 | 52 | workflows: 53 | version: 2 54 | build-test-release: 55 | jobs: 56 | - install 57 | - build: 58 | requires: 59 | - install 60 | - release: 61 | requires: 62 | - build 63 | filters: 64 | branches: 65 | only: master 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | .vscode 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tim Deschryver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxjs-operator-counter 2 | 3 | Count the number of RxJS operators used in an application. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npx rxjs-operator-counter 9 | 10 | # or to define a tsconfig 11 | npx rxjs-operator-counter -p ./project/tsconfig.json 12 | ``` 13 | -------------------------------------------------------------------------------- /bin/rxjs-operator-counter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let minimist = require('minimist') 4 | let path = require('path') 5 | 6 | require(path.join(__dirname, '..', 'crawler')).crawl({ 7 | tsConfigPath: minimist(process.argv.slice(2))['p'], 8 | }) 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-operator-counter", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Count the number of RxJS operators used in an application", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/timdeschryver/rxjs-operator-counter.git" 8 | }, 9 | "author": "Tim Deschryver", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/timdeschryver/rxjs-operator-counter/issues" 13 | }, 14 | "homepage": "https://github.com/timdeschryver/rxjs-operator-counter#readme", 15 | "bin": "./bin/rxjs-operator-counter", 16 | "scripts": { 17 | "prebuild": "rimraf dist", 18 | "build": "yarn test && tsc", 19 | "postbuild": "cp -r bin dist && cp package.json dist && cp README.md dist && cp LICENSE dist", 20 | "test": "jest", 21 | "semantic-release": "semantic-release" 22 | }, 23 | "release": { 24 | "pkgRoot": "dist" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^23.3.10", 28 | "@types/minimist": "^1.2.0", 29 | "@types/node": "^10.12.18", 30 | "jest": "^23.6.0", 31 | "prettier": "^1.15.3", 32 | "rimraf": "^2.6.2", 33 | "semantic-release": "^15.13.1", 34 | "ts-jest": "^23.10.5", 35 | "tslint-config-prettier": "^1.17.0", 36 | "tslint-config-standard": "^8.0.1" 37 | }, 38 | "dependencies": { 39 | "@phenomnomnominal/tsquery": "^3.0.0", 40 | "kleur": "^3.0.1", 41 | "minimist": "^1.2.0", 42 | "tslint": "^5.12.0", 43 | "typescript": "^3.2.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | trailingComma: 'es5', 4 | tabWidth: 2, 5 | semi: false, 6 | singleQuote: true, 7 | bracketSpacing: true, 8 | }; 9 | -------------------------------------------------------------------------------- /src/crawler.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint' 2 | import * as path from 'path' 3 | import { EOL } from 'os' 4 | import { white, blue, green } from 'kleur' 5 | 6 | export function crawl({ tsConfigPath = '' } = {}) { 7 | if (!tsConfigPath) { 8 | tsConfigPath = 'tsconfig.json' 9 | console.log( 10 | blue(`No tsconfig provided with -p, using "${tsConfigPath}" as fallback`) 11 | ) 12 | } 13 | 14 | console.log(white('One moment, crawling files to find operators 🧐')) 15 | 16 | const options = { 17 | fix: false, 18 | rulesDirectory: path.join(__dirname, 'rules'), 19 | } 20 | const program = Lint.Linter.createProgram(tsConfigPath, './') 21 | const linter = new Lint.Linter(options, program) 22 | const rules = new Map>([ 23 | [ 24 | 'operator-counter', 25 | { 26 | ruleName: 'operator-counter', 27 | }, 28 | ], 29 | ]) 30 | const lintConfiguration = { 31 | rules, 32 | jsRules: rules, 33 | rulesDirectory: [options.rulesDirectory], 34 | extends: [''], 35 | } 36 | 37 | const files = Lint.Linter.getFileNames(program) 38 | files.forEach(file => { 39 | const fileContents = program.getSourceFile(file).getFullText() 40 | linter.lint(file, fileContents, lintConfiguration) 41 | }) 42 | 43 | const results = linter.getResult() 44 | const hits = results.failures 45 | .map(x => x.getFailure()) 46 | .reduce<{ [operator: string]: number }>((counter, operator) => { 47 | counter[operator] = (counter[operator] || 0) + 1 48 | return counter 49 | }, {}) 50 | 51 | console.log(green().underline(`Results: ${EOL}`)) 52 | 53 | Object.entries(hits) 54 | .sort( 55 | ([key1, count1], [key2, count2]) => 56 | count2 - count1 || key1.localeCompare(key2) 57 | ) 58 | .forEach(([key, count]) => console.log(green().bold(`${key}: ${count}`))) 59 | 60 | return hits 61 | } 62 | -------------------------------------------------------------------------------- /src/rules/operatorCounterRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as Lint from 'tslint' 3 | import { tsquery } from '@phenomnomnominal/tsquery' 4 | 5 | export class Rule extends Lint.Rules.AbstractRule { 6 | static ruleName = 'operator-counter' 7 | static query = 8 | 'CallExpression[expression.name.name="pipe"] > CallExpression > Identifier' 9 | 10 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 11 | const hits = tsquery(sourceFile, Rule.query, { 12 | visitAllChildren: true, 13 | }) 14 | return hits.map( 15 | (operator: ts.Identifier) => 16 | new Lint.RuleFailure( 17 | sourceFile, 18 | operator.getStart(), 19 | operator.getEnd(), 20 | operator.text, 21 | Rule.ruleName 22 | ) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/__snapshots__/crawler.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test run 1`] = ` 4 | "One moment, crawling files to find operators 🧐Results: 5 | map: 4mapTo: 3catchError: 1filter: 1ofType: 1select: 1switchMap: 1" 6 | `; 7 | -------------------------------------------------------------------------------- /tests/crawler.spec.ts: -------------------------------------------------------------------------------- 1 | import { crawl } from '../src/crawler' 2 | 3 | test(`test run`, () => { 4 | let log = '' 5 | console.log = jest.fn(message => (log += message)) 6 | crawl({ 7 | tsConfigPath: './tests/fixtures/tsconfig.json', 8 | }) 9 | 10 | expect(log).toMatchSnapshot() 11 | }) 12 | -------------------------------------------------------------------------------- /tests/fixtures/flamingo.ts: -------------------------------------------------------------------------------- 1 | const foo = of([]).pipe( 2 | map(x => 5), 3 | map(x => 5), 4 | filter(x => true) 5 | ) 6 | -------------------------------------------------------------------------------- /tests/fixtures/pangolin.ts: -------------------------------------------------------------------------------- 1 | @Effect() 2 | getCustomers = this.actions.pipe( 3 | ofType(CustomerActionTypes.Get), 4 | switchMap(() => 5 | this.service.get().pipe( 6 | map(customers => new GetCustomersSuccess(customers)), 7 | catchError(error => of(new GetCustomersFailed(error))), 8 | ), 9 | ), 10 | ); 11 | 12 | @Effect() 13 | ping = interval(1000).pipe(mapTo(new Ping())); 14 | 15 | @Effect() 16 | online = merge( 17 | of(navigator.onLine), 18 | fromEvent(window, 'online').pipe(mapTo(true)), 19 | fromEvent(window, 'offline').pipe(mapTo(false)), 20 | ).pipe(map(online => online ? new IsOnline() : new IsOffline())); 21 | 22 | this.customers = this.store.pipe(select(getCustomers)); 23 | -------------------------------------------------------------------------------- /tests/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["."] 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "typeRoots": ["node_modules/@types/"], 5 | "target": "es5", 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "noLib": false, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "declaration": true, 12 | "removeComments": true, 13 | "lib": ["es6", "es2017.object"] 14 | }, 15 | "exclude": ["node_modules"], 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"] 3 | } --------------------------------------------------------------------------------