├── .vscode ├── settings.json └── launch.json ├── update_test_expectations.sh ├── bin └── jstyper.js ├── .babelrc ├── .travis.yml ├── src ├── utils │ ├── strings.ts │ ├── mutator.ts │ ├── files.ts │ ├── symbols.ts │ ├── maps.ts │ ├── name_guesser.ts │ ├── pseudo_json.ts │ ├── versioned_file.ts │ ├── nodes.ts │ ├── flags.ts │ ├── type_predicates.ts │ ├── call_constraints.ts │ ├── language_service_reactor.ts │ ├── apply_constraints.ts │ ├── operators.ts │ ├── constraints_cache.ts │ ├── type_constraints.ts │ └── node_predicates.ts ├── options.ts ├── passes │ ├── update_vars.ts │ ├── update_imports.ts │ ├── format.ts │ ├── declarations.ts │ ├── update_exports.ts │ └── infer.ts ├── testing │ ├── support.ts │ └── test_spec.ts ├── editor │ ├── indent.ts │ └── undo.ts ├── cli.ts ├── typer.ts ├── matchers │ └── objects.ts └── demo.ts ├── test ├── test_specs.ts └── specs │ ├── normalizes arrow function syntax.ts │ ├── types imported modules.ts │ ├── detects voids.ts │ ├── types required modules.ts │ ├── infers from typeof checks.ts │ ├── creates classes.ts │ ├── does not propagate undefined in callers.ts │ ├── computed properties are confused with properties by default.ts │ ├── recognizes Object.create.ts │ ├── recognizes optional params.ts │ ├── recognizes writable properties.ts │ ├── propagates method param types.ts │ ├── propagates vars backwards.ts │ ├── picks good argument names.ts │ ├── computed properties are differentiated from properties.ts │ ├── infers members called on parameters.ts │ ├── recognizes Object.assign.ts │ ├── rewrites requires.ts │ ├── generates interfaces.ts │ ├── respects class symbols.ts │ ├── forwards member detection.ts │ ├── detects string methods.ts │ ├── resolves imports.ts │ ├── rewrites exports.ts │ ├── recognizes Object.defineProperty.ts │ └── infers from operators.ts ├── deploy.sh ├── rollup.config.cli.js ├── .gitignore ├── tsconfig.json ├── rollup.config.demo.js ├── package.json ├── index.html └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /update_test_expectations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | for file in test/data/*.js ; do 5 | node build/main.js $file 6 | done -------------------------------------------------------------------------------- /bin/jstyper.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | global.fs = require('fs') 3 | global.path = require('path') 4 | global.ts = require('typescript') 5 | 6 | require('../build/cli'); -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "external-helpers" 12 | ] 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | cache: 5 | bundler: true 6 | directories: 7 | - node_modules 8 | # - $HOME/.nvm 9 | # - $HOME/.npm 10 | 11 | before_cache: 12 | - du -h -d 1 node_modules 13 | # - du -h -d 1 $HOME/.nvm 14 | # - du -h -d 1 $HOME/.npm -------------------------------------------------------------------------------- /src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | 2 | export function deindent(src: string) { 3 | src = src.replace(/\t/g, ' '); 4 | const match = /^[ \t]*\n( +)([\s\S]*)$/m.exec(src); 5 | if (match) { 6 | const indent = match[1]; 7 | src = match[2]; 8 | const result = src.replace(new RegExp(`^${indent}`, 'gm'), ''); 9 | return result; 10 | } 11 | return src; 12 | } 13 | -------------------------------------------------------------------------------- /test/test_specs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {typerTest} from '../src/testing/support'; 3 | 4 | require('source-map-support').install(); 5 | 6 | describe('Typer', () => { 7 | const specsDir = 'test/specs'; 8 | for (const file of fs.readdirSync(specsDir).filter(n => n.endsWith('.ts'))) { 9 | it(file, typerTest(`${specsDir}/${file}`)); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | readonly TEMP_DIR="$PWD/tmp/ochafik.com" 5 | readonly RELATIVE_DEMO="assets/typer-demo.html" 6 | 7 | # npm run prepare-release 8 | 9 | rm -fR "${TEMP_DIR}" 10 | mkdir -p "${TEMP_DIR}" 11 | git clone --depth 1 git@github.com:ochafik/ochafik.github.io.git "$TEMP_DIR" 12 | 13 | cp build/demo.html "${TEMP_DIR}/${RELATIVE_DEMO}" 14 | ( 15 | cd "${TEMP_DIR}" 16 | git status 17 | git commit -m "Update demo" "${RELATIVE_DEMO}" 18 | git push 19 | ) -------------------------------------------------------------------------------- /test/specs/normalizes arrow function syntax.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'foo.ts': ` 10 | x => x * 2; 11 | (x) => x * 2; 12 | x/*what*/=> x * 2; 13 | ` 14 | }, 15 | options: {}, 16 | result: { 17 | files: { 18 | 'foo.ts': ` 19 | (x: number) => x * 2; 20 | (x: number) => x * 2; 21 | x/*what*/=> x * 2; 22 | ` 23 | }, 24 | metadata: { 25 | inferencePasses: 2 26 | } 27 | } 28 | } as TestSpec 29 | -------------------------------------------------------------------------------- /rollup.config.cli.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript'; 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | import filesize from 'rollup-plugin-filesize'; 4 | import babel from 'rollup-plugin-babel'; 5 | 6 | export default { 7 | entry: './src/cli.ts', 8 | dest: 'build/cli.js', 9 | format: 'iife', 10 | external: ['typescript', 'fs', 'path'], 11 | globals: { 12 | typescript: 'ts', 13 | fs: 'fs', 14 | path: 'path', 15 | }, 16 | plugins: [ 17 | typescript({typescript: require('typescript')}), 18 | nodeResolve({jsnext: true, main: true, browser: true}), 19 | babel({exclude: 'node_modules/**'}), 20 | filesize() 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/mutator.ts: -------------------------------------------------------------------------------- 1 | import {AddChangeCallback} from './language_service_reactor'; 2 | 3 | export class Mutator { 4 | constructor(private fileName: string, private addChange: AddChangeCallback) {} 5 | 6 | insert(start: number, newText: string) { 7 | this.addChange( 8 | this.fileName, {span: {start: start, length: 0}, newText: newText}); 9 | } 10 | 11 | removeNode(node: {getStart(): number, getEnd(): number}, newText: string = '') { 12 | this.remove({start: node.getStart(), end: node.getEnd()}, newText); 13 | } 14 | 15 | remove({start, end}: {start: number, end: number}, newText: string = '') { 16 | this.addChange( 17 | this.fileName, 18 | {span: {start: start, length: end - start}, newText: newText}); 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | build/* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | tmp/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Lancer le programme", 11 | "program": "${workspaceRoot}/build/main.js", 12 | "args": [ 13 | "test/data/input.js" 14 | ], 15 | "cwd": "${workspaceRoot}", 16 | "outFiles": [] 17 | }, 18 | { 19 | "type": "node", 20 | "request": "attach", 21 | "name": "Attacher au processus", 22 | "port": 5858, 23 | "outFiles": [] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /test/specs/types imported modules.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.ts': ` 10 | import {a, b, c} from 'foo'; 11 | 12 | a && a(); 13 | b(1) == 2; 14 | c = 2; 15 | 16 | 17 | ` 18 | }, 19 | options: {}, 20 | result: { 21 | files: { 22 | 'input.ts': ` 23 | import {a, b, c} from 'foo'; 24 | 25 | a && a(); 26 | b(1) == 2; 27 | c = 2; 28 | 29 | 30 | ` 31 | }, 32 | metadata: { 33 | inferencePasses: 1 34 | } 35 | } 36 | } as TestSpec 37 | -------------------------------------------------------------------------------- /src/utils/files.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export function mkdirsSync(dir: string) { 5 | dir.split(path.sep).reduce((d, component) => { 6 | const dir = (d && d + '/' || '') + component; 7 | if (!fs.existsSync(dir)) { 8 | fs.mkdirSync(dir); 9 | } 10 | return dir; 11 | }, ''); 12 | } 13 | 14 | export function getFile( 15 | fileName: string, 16 | {outputDir, currentWorkingDir}: 17 | {outputDir?: string, currentWorkingDir?: string} = {}): string { 18 | let outputFile = fileName; 19 | if (outputDir) { 20 | outputFile = 21 | path.join(outputDir, path.relative(currentWorkingDir == null ? '.' : currentWorkingDir, fileName)); 22 | } 23 | mkdirsSync(path.dirname(outputFile)); 24 | return outputFile; 25 | } -------------------------------------------------------------------------------- /src/utils/symbols.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as nodes from '../utils/nodes'; 3 | 4 | export function getSymbol(node: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { 5 | if (nodes.isConstructor(node) || nodes.isArrowFunction(node) || 6 | nodes.isFunctionExpression(node)) { 7 | const sym = node['symbol']; // this.checker.getSymbolAtLocation(node); 8 | return sym; 9 | } else if (nodes.isVariableDeclaration(node)) { 10 | return getSymbol(node.name, checker); 11 | } else if (nodes.isFunctionLikeDeclaration(node) || nodes.isInterfaceDeclaration(node)) { 12 | return node.name && getSymbol(node.name, checker); 13 | } else if (nodes.isIdentifier(node)) { 14 | return checker.getSymbolAtLocation(node); 15 | } else { 16 | return undefined; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "build", 7 | "forceConsistentCasingInFileNames": true, 8 | "importHelpers": true, 9 | "noImplicitAny": false, 10 | "noImplicitThis": true, 11 | "noImplicitReturns": true, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "alwaysStrict": true, 16 | "pretty": true, 17 | "removeComments": true, 18 | "sourceMap": true, 19 | "allowJs": false, 20 | "strictNullChecks": true 21 | }, 22 | "atom": { 23 | "rewriteTsconfig": true 24 | }, 25 | "include": [ 26 | "src/**/*", 27 | "test/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/specs/detects voids.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f3(x) { 11 | g3(1); 12 | g3(x); 13 | } 14 | 15 | function g3(x) { 16 | return x + 1; 17 | } 18 | 19 | 20 | ` 21 | }, 22 | options: {}, 23 | result: { 24 | files: { 25 | 'input.js': ` 26 | function f3(x: number): void { 27 | g3(1); 28 | g3(x); 29 | } 30 | 31 | function g3(x: number): number { 32 | return x + 1; 33 | } 34 | 35 | 36 | ` 37 | }, 38 | metadata: { 39 | inferencePasses: 3 40 | } 41 | } 42 | } as TestSpec 43 | -------------------------------------------------------------------------------- /test/specs/types required modules.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | var foo = require('foo'); 11 | var a = foo.a; 12 | var b = foo.b; 13 | 14 | a(10) == ''; 15 | b == 3; 16 | foo.c = 10; 17 | 18 | 19 | ` 20 | }, 21 | options: {}, 22 | result: { 23 | files: { 24 | 'input.js': ` 25 | import * as foo from 'foo'; 26 | var a: (arg1: number) => string = foo.a; 27 | var b: number = foo.b; 28 | 29 | a(10) == ''; 30 | b == 3; 31 | foo.c = 10; 32 | 33 | 34 | ` 35 | }, 36 | metadata: { 37 | inferencePasses: 2 38 | } 39 | } 40 | } as TestSpec 41 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | format: boolean, 3 | differentiateComputedProperties: boolean, 4 | updateImports: boolean, 5 | updateExports: boolean, 6 | declarations: boolean, 7 | updateVars: boolean, 8 | maxIterations: number, 9 | debugPasses: boolean, 10 | // maxSubInferenceCount: number, 11 | currentWorkingDir: string, 12 | methodThresholdAfterWhichAssumeString: number, 13 | dependenciesFileName: string, 14 | }; 15 | 16 | export const defaultOptions: Readonly = Object.freeze({ 17 | format: true, 18 | differentiateComputedProperties: false, 19 | updateImports: true, 20 | updateExports: true, 21 | declarations: false, 22 | updateVars: false, // Not ready! 23 | maxIterations: 5, 24 | debugPasses: false, 25 | // maxSubInferenceCount: 5, 26 | currentWorkingDir: '.', 27 | methodThresholdAfterWhichAssumeString: 1, 28 | dependenciesFileName: 'dependencies.d.ts', 29 | }); -------------------------------------------------------------------------------- /test/specs/infers from typeof checks.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | (x) => typeof x == 'undefined'; 11 | (x) => typeof x == 'string'; 12 | (x) => typeof x == 'boolean'; 13 | (x) => typeof x == 'number'; 14 | (x) => typeof x == 'symbol'; 15 | ` 16 | }, 17 | options: {}, 18 | result: { 19 | files: { 20 | 'input.js': ` 21 | (x?: any) => typeof x == 'undefined'; 22 | (x: string) => typeof x == 'string'; 23 | (x: boolean) => typeof x == 'boolean'; 24 | (x: number) => typeof x == 'number'; 25 | (x: symbol) => typeof x == 'symbol'; 26 | ` 27 | }, 28 | metadata: { 29 | inferencePasses: 2 30 | } 31 | } 32 | } as TestSpec 33 | -------------------------------------------------------------------------------- /test/specs/creates classes.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | import {Foo} from 'foo'; 11 | new Foo(1).x = 1; 12 | 13 | var Bar = function(x) {}; 14 | new Bar('').y = 1; 15 | 16 | function Baz(x) {} 17 | new Baz(true).z = 1; 18 | ` 19 | }, 20 | options: {}, 21 | result: { 22 | files: { 23 | 'input.js': ` 24 | import {Foo} from 'foo'; 25 | new Foo(1).x = 1; 26 | 27 | var Bar: new(x: string) => void = function(x: string): void {}; 28 | new Bar('').y = 1; 29 | 30 | function Baz(x: boolean): void {} 31 | new Baz(true).z = 1; 32 | ` 33 | }, 34 | metadata: { 35 | inferencePasses: 3 36 | } 37 | } 38 | } as TestSpec 39 | -------------------------------------------------------------------------------- /rollup.config.demo.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import filesize from 'rollup-plugin-filesize'; 5 | import { minify } from 'uglify-js-harmony'; 6 | import babel from 'rollup-plugin-babel'; 7 | 8 | const isDev = process.env.DEV == '1' 9 | console.warn(`DEV: ${isDev}`); 10 | 11 | export default { 12 | entry: './src/demo.ts', 13 | dest: 'build/demo.js', 14 | format: 'iife', 15 | sourceMap: true, 16 | external: ['typescript'], 17 | globals: { 18 | typescript: 'ts' 19 | }, 20 | plugins: [ 21 | typescript({typescript: require('typescript')}), 22 | nodeResolve({ 23 | jsnext: true, 24 | main: true, 25 | browser: true 26 | }), 27 | ...(isDev ? [] : [ 28 | babel({exclude: 'node_modules/**'}), 29 | uglify({sourceMap: true}, minify), 30 | ]), 31 | filesize() 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/specs/does not propagate undefined in callers.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function g1(x) { return x * 2; } 11 | function f1(x) { g1(x); g1(); } 12 | 13 | function g2(x) { return x === null ? 1 : x * 2; } 14 | function f2(x) { g2(x); g2(); } 15 | ` 16 | }, 17 | options: {}, 18 | result: { 19 | files: { 20 | 'input.js': ` 21 | function g1(x?: number): number { return x * 2; } 22 | function f1(x: number): void { g1(x); g1(); } 23 | 24 | function g2(x?: number | null): number { return x === null ? 1 : x * 2; } 25 | function f2(x: number): void { g2(x); g2(); } 26 | ` 27 | }, 28 | metadata: { 29 | inferencePasses: 3 30 | } 31 | } 32 | } as TestSpec 33 | -------------------------------------------------------------------------------- /test/specs/computed properties are confused with properties by default.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f(x) { 11 | x.a.foo; 12 | x['a'].bar; 13 | x.b().foo; 14 | x['b']().bar; 15 | 'z' in x; 16 | } 17 | ` 18 | }, 19 | options: {}, 20 | result: { 21 | files: { 22 | 'input.js': ` 23 | function f(x: {readonly a: {readonly bar: any, readonly foo: any}, b(): {readonly bar: any, readonly foo: any}, readonly z?: any}): void { 24 | x.a.foo; 25 | x['a'].bar; 26 | x.b().foo; 27 | x['b']().bar; 28 | 'z' in x; 29 | } 30 | ` 31 | }, 32 | metadata: { 33 | inferencePasses: 2 34 | } 35 | } 36 | } as TestSpec 37 | -------------------------------------------------------------------------------- /test/specs/recognizes Object.create.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | let a = {x: 1}; 11 | let b = Object.create(a); 12 | let c = Object.create(a, {y: 2}) 13 | let d = Object.create({}, a) 14 | 15 | let e = Object.create() 16 | ` 17 | }, 18 | options: { 19 | differentiateComputedProperties: true 20 | }, 21 | result: { 22 | files: { 23 | 'input.js': ` 24 | let a: {x: number} = {x: 1}; 25 | let b: {x: number} = Object.create(a); 26 | let c: {x: number, y: number} = Object.create(a, {y: 2}) 27 | let d: {x: number} = Object.create({}, a) 28 | 29 | let e = Object.create() 30 | ` 31 | }, 32 | metadata: { 33 | inferencePasses: 2 34 | } 35 | } 36 | } as TestSpec 37 | -------------------------------------------------------------------------------- /test/specs/recognizes optional params.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f6(x) { 11 | return x && x.y(1) + x.y(1, 2); 12 | } 13 | 14 | function g6(x) { 15 | return x && x.y && x.y(1); 16 | } 17 | 18 | 19 | ` 20 | }, 21 | options: {}, 22 | result: { 23 | files: { 24 | 'input.js': ` 25 | function f6(x?: {y(arg1: number, arg2?: number): any}) { 26 | return x && x.y(1) + x.y(1, 2); 27 | } 28 | 29 | function g6(x?: {readonly y?: (arg1: number) => boolean}): boolean { 30 | return x && x.y && x.y(1); 31 | } 32 | 33 | 34 | ` 35 | }, 36 | metadata: { 37 | inferencePasses: 3 38 | } 39 | } 40 | } as TestSpec 41 | -------------------------------------------------------------------------------- /src/utils/maps.ts: -------------------------------------------------------------------------------- 1 | export function mapToObject(map: Map): {[key: string]: V} { 2 | const result: {[key: string]: V} = Object.create(null); 3 | for (const [k, v] of map) { 4 | result[k] = v; 5 | } 6 | return result; 7 | } 8 | 9 | export function objectToMap(obj: Map): Map { 10 | const result = new Map(); 11 | for (const k in obj) { 12 | result.set(k, obj[k]); 13 | } 14 | return result; 15 | } 16 | 17 | export function mapValues( 18 | obj: {[key: string]: U}, f: (U) => V): {[key: string]: V} { 19 | const result = Object.create(null); 20 | for (const key of Object.keys(obj)) { 21 | result[key] = f(obj[key]); 22 | } 23 | return result; 24 | } 25 | 26 | export function mapKeys( 27 | obj: {[key: string]: V}, f: (key: string) => string): {[key: string]: V} { 28 | const result = Object.create(null); 29 | for (const key of Object.keys(obj)) { 30 | result[f(key)] = obj[key]; 31 | } 32 | return result; 33 | } 34 | -------------------------------------------------------------------------------- /test/specs/recognizes writable properties.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f(x) { 11 | x.a; 12 | x.b = 1; 13 | if (x.c) x.c(); 14 | delete x.d; 15 | } 16 | 17 | function g(x) { 18 | f(x); 19 | } 20 | ` 21 | }, 22 | options: {}, 23 | result: { 24 | files: { 25 | 'input.js': ` 26 | function f(x: {readonly a: any, b: number, readonly c?: () => void, d?: any}): void { 27 | x.a; 28 | x.b = 1; 29 | if (x.c) x.c(); 30 | delete x.d; 31 | } 32 | 33 | function g(x: {readonly a: any, b: number, readonly c?: () => void, d?: any}): void { 34 | f(x); 35 | } 36 | ` 37 | }, 38 | metadata: { 39 | inferencePasses: 3 40 | } 41 | } 42 | } as TestSpec 43 | -------------------------------------------------------------------------------- /test/specs/propagates method param types.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'example.ts': ` 10 | class Foo { 11 | constructor(x) { 12 | x.bar(); 13 | } 14 | foo(x) { 15 | return x * 2; 16 | } 17 | set x(v) { 18 | v == ' '; 19 | } 20 | } 21 | 22 | 23 | ` 24 | }, 25 | options: {}, 26 | result: { 27 | files: { 28 | 'example.ts': ` 29 | class Foo { 30 | constructor(x: {bar(): void}) { 31 | x.bar(); 32 | } 33 | foo(x: number): number { 34 | return x * 2; 35 | } 36 | set x(v: string) { 37 | v == ' '; 38 | } 39 | } 40 | 41 | 42 | ` 43 | }, 44 | metadata: { 45 | inferencePasses: 2 46 | } 47 | } 48 | } as TestSpec 49 | -------------------------------------------------------------------------------- /src/passes/update_vars.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {ReactorCallback} from '../utils/language_service_reactor'; 3 | import * as nodes from '../utils/nodes'; 4 | 5 | export const updateVars: ReactorCallback = (fileNames, services, addChange, _) => { 6 | const program = services.getProgram(); 7 | 8 | for (const sourceFile of program.getSourceFiles()) { 9 | if (fileNames.indexOf(sourceFile.fileName) >= 0) { 10 | nodes.traverse(sourceFile, (node: ts.Node) => { 11 | if (nodes.isVariableStatement(node)) { 12 | if (node.modifiers) { 13 | for (const mod of node.modifiers) { 14 | if (nodes.isVarKeyword(mod)) { 15 | addChange(sourceFile.fileName, { 16 | span: {start: mod.getStart(), length: mod.getFullWidth()}, 17 | newText: 'let' 18 | }); 19 | return; 20 | } 21 | } 22 | } 23 | // console.log(`FOUND DECLS: ${varDecls.getFullText()}`); 24 | } 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/name_guesser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as nodes from './nodes'; 3 | 4 | export function guessName(node: ts.Node): string|undefined { 5 | if (nodes.isCallExpression(node)) { 6 | let name: string|undefined; 7 | if (node.name) { 8 | name = guessName(node.name); 9 | } else if (node.expression) { 10 | name = guessName(node.expression); 11 | } 12 | if (name) { 13 | const exec = /^(?:(?:get|set|build|create|new)_*)(.*)$/.exec(name); 14 | if (exec) { 15 | const n = exec[1]; 16 | if (n) { 17 | name = n[0].toLowerCase() + n.substr(1); 18 | } 19 | } 20 | return name; 21 | } 22 | } else if (nodes.isElementAccessExpression(node)) { 23 | if (node.argumentExpression && 24 | nodes.isStringLiteral(node.argumentExpression)) { 25 | return node.argumentExpression.text; 26 | } 27 | } else if (nodes.isPropertyAccessExpression(node)) { 28 | return guessName(node.name); 29 | } else if (nodes.isIdentifier(node)) { 30 | return node.text; 31 | } 32 | return undefined; 33 | } 34 | -------------------------------------------------------------------------------- /test/specs/propagates vars backwards.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f(xx, y) { 11 | var zz1; 12 | zz1 = z; 13 | zz1 = ''; 14 | 15 | var zz2 = z; 16 | zz2 = ''; 17 | return x + 2 + g(y); 18 | } 19 | 20 | function g(x) { 21 | return x * 2; 22 | } 23 | 24 | ` 25 | }, 26 | options: {}, 27 | result: { 28 | files: { 29 | 'input.js': ` 30 | function f(xx, y: number) { 31 | var zz1: string; 32 | zz1 = z; 33 | zz1 = ''; 34 | 35 | var zz2: string = z; 36 | zz2 = ''; 37 | return x + 2 + g(y); 38 | } 39 | 40 | function g(x: number): number { 41 | return x * 2; 42 | } 43 | 44 | ` 45 | }, 46 | metadata: { 47 | inferencePasses: 3 48 | } 49 | } 50 | } as TestSpec 51 | -------------------------------------------------------------------------------- /src/passes/update_imports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {ReactorCallback} from '../utils/language_service_reactor'; 3 | import * as nodes from '../utils/nodes'; 4 | import {Mutator} from '../utils/mutator'; 5 | 6 | export const updateImports: ReactorCallback = (fileNames, services, addChange, _) => { 7 | const program = services.getProgram(); 8 | 9 | for (const sourceFile of program.getSourceFiles()) { 10 | if (fileNames.indexOf(sourceFile.fileName) >= 0) { 11 | const mutator = new Mutator(sourceFile.fileName, addChange); 12 | 13 | nodes.traverse(sourceFile, (node: ts.Node) => { 14 | if (nodes.isVariableStatement(node) && node.declarationList.declarations.length == 1) { 15 | const [decl] = node.declarationList.declarations; 16 | if (nodes.isIdentifier(decl.name)) { 17 | const requiredPath = nodes.getRequiredPath(decl.initializer); 18 | if (requiredPath) { 19 | mutator.removeNode(node, `import * as ${decl.name.text} from '${requiredPath}';`); 20 | } 21 | } 22 | } 23 | }); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/specs/picks good argument names.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f8(x, y, z) { 11 | x.foo(y); 12 | x.bar(z.getBaz()); 13 | x.bam(z['yay']); 14 | 15 | let count, superCount, megaCount; 16 | x.sum(superCount); 17 | x.sum(count); 18 | x.sum(megaCount); 19 | } 20 | 21 | ` 22 | }, 23 | options: {}, 24 | result: { 25 | files: { 26 | 'input.js': ` 27 | function f8(x: {bam(yay: any): void, bar(baz: any): void, foo(y: any): void, sum(count?: any): void}, y, z: {getBaz(): any, readonly yay: any}): void { 28 | x.foo(y); 29 | x.bar(z.getBaz()); 30 | x.bam(z['yay']); 31 | 32 | let count, superCount, megaCount; 33 | x.sum(superCount); 34 | x.sum(count); 35 | x.sum(megaCount); 36 | } 37 | 38 | ` 39 | }, 40 | metadata: { 41 | inferencePasses: 2 42 | } 43 | } 44 | } as TestSpec 45 | -------------------------------------------------------------------------------- /test/specs/computed properties are differentiated from properties.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f(x, z) { 11 | x.a == 1; 12 | x['a'] == ''; 13 | x.b(); 14 | x['b'](); 15 | x.c = true; 16 | x['c'] = ''; 17 | 'z' in x; 18 | 19 | z['yay']; 20 | } 21 | ` 22 | }, 23 | options: { 24 | differentiateComputedProperties: true 25 | }, 26 | result: { 27 | files: { 28 | 'input.js': ` 29 | function f(x: {readonly a: number, b(): void, c: boolean, readonly ['a']: number | string, ['b'](): void, ['c']: string, readonly ['z']?: any}, z: {readonly ['yay']: any}): void { 30 | x.a == 1; 31 | x['a'] == ''; 32 | x.b(); 33 | x['b'](); 34 | x.c = true; 35 | x['c'] = ''; 36 | 'z' in x; 37 | 38 | z['yay']; 39 | } 40 | ` 41 | }, 42 | metadata: { 43 | inferencePasses: 3 44 | } 45 | } 46 | } as TestSpec 47 | -------------------------------------------------------------------------------- /test/specs/infers members called on parameters.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f(x, y) { 11 | console.log(x, y); 12 | var z = x.call(1, 2); 13 | y.foo(); 14 | g(z); 15 | g(x.memberOfX); 16 | y(1, 2, 3); 17 | } 18 | 19 | function g(x) { 20 | return x * 2; 21 | } 22 | 23 | 24 | ` 25 | }, 26 | options: {}, 27 | result: { 28 | files: { 29 | 'input.js': ` 30 | function f(x: {call(arg1: number, arg2: number): number, readonly memberOfX: number}, y: {(arg1: number, arg2: number, arg3: number): void, foo(): void}): void { 31 | console.log(x, y); 32 | var z: number = x.call(1, 2); 33 | y.foo(); 34 | g(z); 35 | g(x.memberOfX); 36 | y(1, 2, 3); 37 | } 38 | 39 | function g(x: number): number { 40 | return x * 2; 41 | } 42 | 43 | 44 | ` 45 | }, 46 | metadata: { 47 | inferencePasses: 4 48 | } 49 | } 50 | } as TestSpec 51 | -------------------------------------------------------------------------------- /test/specs/recognizes Object.assign.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'readonlies.js': ` 10 | function f(x, y) { 11 | console.log(x.i); 12 | Object.assign(x, y, {a: '', b: 1, ['c']: true}); 13 | } 14 | f({z: 1, a: ''}, {k: 1}); 15 | 16 | function f_ret() { 17 | return Object.assign({}, {a: 1}); 18 | } 19 | let x_assigned = Object.assign({}, {a: 1}); 20 | ` 21 | }, 22 | options: { 23 | differentiateComputedProperties: true 24 | }, 25 | result: { 26 | files: { 27 | 'readonlies.js': ` 28 | function f(x: {a?: string, b?: number, readonly i: any, k?: number, readonly z: number, ['c']: boolean}, y: {readonly k: number}): void { 29 | console.log(x.i); 30 | Object.assign(x, y, {a: '', b: 1, ['c']: true}); 31 | } 32 | f({z: 1, a: ''}, {k: 1}); 33 | 34 | function f_ret(): {a?: number} { 35 | return Object.assign({}, {a: 1}); 36 | } 37 | let x_assigned: {a?: number} = Object.assign({}, {a: 1}); 38 | ` 39 | }, 40 | metadata: { 41 | inferencePasses: 3 42 | } 43 | } 44 | } as TestSpec 45 | -------------------------------------------------------------------------------- /test/specs/rewrites requires.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'foo.js': ` 10 | var x = require('./bar').x; 11 | var x1 = require('./bar').x; 12 | 13 | var bar = require('./bar'); 14 | var barx = bar.x; 15 | 16 | var bar2 = require('./bar'); 17 | var bar2x = bar2.x; 18 | 19 | var bar3 = require('./bar'), 20 | bar3x = bar3.x, 21 | bar3y = bar3.y; 22 | `, 23 | 'bar.js': ` 24 | module.exports = { x: 1, y: 2 }; 25 | ` 26 | }, 27 | options: {}, 28 | result: { 29 | files: { 30 | 'foo.js': ` 31 | var x = require('./bar').x; 32 | var x1 = require('./bar').x; 33 | 34 | import * as bar from './bar'; 35 | var barx = bar.x; 36 | 37 | import * as bar2 from './bar'; 38 | var bar2x = bar2.x; 39 | 40 | var bar3: {readonly x: any, readonly y: any} = require('./bar'), 41 | bar3x = bar3.x, 42 | bar3y = bar3.y; 43 | `, 44 | 'bar.js': ` 45 | export default { x: 1, y: 2 }; 46 | ` 47 | }, 48 | metadata: { 49 | inferencePasses: 2 50 | } 51 | } 52 | } as TestSpec 53 | -------------------------------------------------------------------------------- /src/testing/support.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as mocha from 'mocha'; 3 | 4 | import {runTyper} from '../typer'; 5 | import {defaultOptions} from '../options'; 6 | 7 | import {readSpec, TestSpec, writeSpec} from './test_spec'; 8 | 9 | const updateSpecs: boolean = process.env.UPDATE_SPECS == '1'; 10 | 11 | export function typerTest(specFile: string): 12 | (this: mocha.ITestCallbackContext) => Promise { 13 | return async function() { 14 | const builtFile = 15 | process.cwd() + '/' + specFile.replace(/^(.*?)\.ts$/, 'build/$1.js'); 16 | const spec = readSpec(builtFile); 17 | if (!spec) { 18 | throw new Error(`Unable to read ${builtFile}`); 19 | } 20 | 21 | const result = runTyper(spec.files, {...defaultOptions, format: false, ...spec.options}); 22 | 23 | const actualSpec: TestSpec = { 24 | files: spec.files, 25 | options: spec.options, 26 | result: result 27 | }; 28 | 29 | if (updateSpecs) { 30 | await writeSpec(specFile, actualSpec); 31 | } else { 32 | for (const fileName in actualSpec.result.files) { 33 | expect(actualSpec.result.files[fileName]).to.be.deep.equal(spec.result.files[fileName]); 34 | } 35 | expect(actualSpec).to.be.deep.equal(spec); 36 | // assert.deepEqual(actualSpec, spec); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /test/specs/generates interfaces.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | var x = 1; 11 | x.y.z(); 12 | 13 | function f(x) { 14 | return x == 2; 15 | } 16 | 17 | class Foo { 18 | x: string = ''; 19 | constructor(x) { 20 | x.a(); 21 | } 22 | foo(x) { 23 | x.b(); 24 | } 25 | get y() { 26 | return 1; 27 | } 28 | set y(v) { 29 | this.x = v == 1 ? 2 : 3; 30 | } 31 | } 32 | 33 | 34 | ` 35 | }, 36 | options: { 37 | declarations: true 38 | }, 39 | result: { 40 | files: { 41 | 'input.d.ts': ` 42 | declare var x: number | ({readonly y: {z(): void}}); 43 | 44 | 45 | declare function f(x: number): boolean ; 46 | 47 | declare class Foo { 48 | x: string; 49 | constructor(x: {a(): void}) ; 50 | foo(x: {b(): void}): void ; 51 | get y() ; 52 | set y(v) ; 53 | } 54 | 55 | 56 | ` 57 | }, 58 | metadata: { 59 | inferencePasses: 2 60 | } 61 | } 62 | } as TestSpec 63 | -------------------------------------------------------------------------------- /test/specs/respects class symbols.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | class Foo { 11 | get x() { return 1 } 12 | y: number; 13 | } 14 | 15 | function f(foo: Foo) { 16 | foo.x == 1; 17 | foo.y == 1; 18 | } 19 | 20 | function f7(foo: Foo) { 21 | return foo ? foo.x : null; 22 | } 23 | 24 | function g7(foo) { 25 | return foo ? foo.x : null; 26 | } 27 | 28 | g7(new Foo()); 29 | 30 | 31 | ` 32 | }, 33 | options: {}, 34 | result: { 35 | files: { 36 | 'input.js': ` 37 | class Foo { 38 | get x() { return 1 } 39 | y: number; 40 | } 41 | 42 | function f(foo: Foo): void { 43 | foo.x == 1; 44 | foo.y == 1; 45 | } 46 | 47 | function f7(foo?: Foo): number | null { 48 | return foo ? foo.x : null; 49 | } 50 | 51 | function g7(foo?: Foo | ({readonly x: number})): number | null { 52 | return foo ? foo.x : null; 53 | } 54 | 55 | g7(new Foo()); 56 | 57 | 58 | ` 59 | }, 60 | metadata: { 61 | inferencePasses: 3 62 | } 63 | } 64 | } as TestSpec 65 | -------------------------------------------------------------------------------- /src/passes/format.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {ReactorCallback} from '../utils/language_service_reactor'; 3 | 4 | export const format: ReactorCallback = (fileNames, services, addChange, _) => { 5 | for (const fileName of fileNames) { 6 | services.getFormattingEditsForDocument(fileName, formattingOptions) 7 | .forEach(c => addChange(fileName, c)); 8 | } 9 | }; 10 | 11 | const formattingOptions: ts.FormatCodeOptions = { 12 | IndentStyle: ts.IndentStyle.Smart, 13 | IndentSize: 2, 14 | TabSize: 2, 15 | BaseIndentSize: 0, 16 | ConvertTabsToSpaces: true, 17 | NewLineCharacter: '\n', 18 | InsertSpaceAfterCommaDelimiter: true, 19 | InsertSpaceAfterSemicolonInForStatements: true, 20 | InsertSpaceBeforeAndAfterBinaryOperators: true, 21 | InsertSpaceAfterConstructor: false, 22 | InsertSpaceAfterKeywordsInControlFlowStatements: true, 23 | InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, 24 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, 25 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, 26 | InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false, 27 | InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: true, 28 | InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: true, 29 | InsertSpaceAfterTypeAssertion: false, 30 | InsertSpaceBeforeFunctionParenthesis: false, 31 | PlaceOpenBraceOnNewLineForFunctions: false, 32 | PlaceOpenBraceOnNewLineForControlBlocks: false, 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/pseudo_json.ts: -------------------------------------------------------------------------------- 1 | 2 | export function pseudoJson( 3 | obj: any, indent: string = '', indentMultilineStrings: boolean = false) { 4 | if (typeof obj == 'number' || typeof obj == 'boolean' || obj == null) { 5 | return `${obj}`; 6 | } else if (typeof obj == 'string') { 7 | let str = obj; 8 | if (obj.indexOf('\n') >= 0 && indentMultilineStrings) { 9 | const lineIndent = '\n' + indent; 10 | str = lineIndent + ' ' + 11 | obj.replace(new RegExp('\n', 'g'), lineIndent + ' ') + lineIndent; 12 | } 13 | return '`' + str.replace(/([$`])/g, '\\$1') + '`'; 14 | } else { 15 | const sub = indent + ' '; 16 | if (obj instanceof Array) { 17 | if (obj.length == 0) { 18 | return '[]'; 19 | } 20 | return '[\n' + indent + 21 | (obj as any[]) 22 | .map(o => pseudoJson(o, sub, indentMultilineStrings)) 23 | .join(',\n' + sub) + 24 | '\n' + indent + ']'; 25 | } else { 26 | const keys = Object.keys(obj); 27 | if (keys.length == 0) { 28 | return '{}'; 29 | } 30 | return '{\n' + sub + 31 | keys.map(key => { 32 | const value = obj[key]; 33 | return (/[^\w]/.test(key) ? 34 | `'` + key.replace(/'\//, '\\$0') + `'` : 35 | key) + 36 | ': ' + pseudoJson(value, sub, indentMultilineStrings); 37 | }) 38 | .join(',\n' + sub) + 39 | '\n' + indent + '}'; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test/specs/forwards member detection.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f4(x) { 11 | return x * 2; 12 | } 13 | 14 | function g4(x, o) { 15 | if (o.addValue) { 16 | return f4(x) + o.value; 17 | } 18 | return o.name == 'default' ? x : 'y'; 19 | } 20 | 21 | function gg4(x, y) { 22 | var v = g4(x, y); 23 | return v; 24 | } 25 | 26 | function h4(x) { 27 | return x ? x.length : 0; 28 | } 29 | 30 | 31 | ` 32 | }, 33 | options: {}, 34 | result: { 35 | files: { 36 | 'input.js': ` 37 | function f4(x: number): number { 38 | return x * 2; 39 | } 40 | 41 | function g4(x: number, o: {readonly addValue: boolean, readonly name: string, readonly value: any}) { 42 | if (o.addValue) { 43 | return f4(x) + o.value; 44 | } 45 | return o.name == 'default' ? x : 'y'; 46 | } 47 | 48 | function gg4(x: number, y: {readonly addValue: boolean, readonly name: string, readonly value: any}) { 49 | var v = g4(x, y); 50 | return v; 51 | } 52 | 53 | function h4(x: string) { 54 | return x ? x.length : 0; 55 | } 56 | 57 | 58 | ` 59 | }, 60 | metadata: { 61 | inferencePasses: 4 62 | } 63 | } 64 | } as TestSpec 65 | -------------------------------------------------------------------------------- /src/utils/versioned_file.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export class VersionedFile { 4 | private _version: number = 0; 5 | private _content: string; 6 | 7 | constructor(public readonly originalContent: string) { 8 | this._content = originalContent; 9 | } 10 | 11 | get version() { 12 | return this._version; 13 | } 14 | 15 | commitChanges(changes: ts.TextChange[]): boolean { 16 | const oldContent = this.content; 17 | const newContent = applyTextChanges(oldContent, changes); 18 | this.content = newContent; 19 | return newContent != oldContent; 20 | } 21 | 22 | get content() { 23 | return this._content; 24 | } 25 | 26 | set content(newContent: string) { 27 | if (this._content == newContent) { 28 | return; 29 | } 30 | this._version++; 31 | this._content = newContent; 32 | } 33 | } 34 | 35 | function applyTextChanges( 36 | initialContent: string, changes: ts.TextChange[]): string { 37 | const inverseSortedChanges = 38 | [...changes].sort((a, b) => b.span.start - a.span.start); 39 | let content = initialContent; 40 | // let lastIndex: number | undefined; 41 | let lastChange: ts.TextChange | undefined; 42 | for (const change of inverseSortedChanges) { 43 | if (lastChange && change.span.start === lastChange.span.start) { 44 | console.warn(`Concurrent changes: ${JSON.stringify(lastChange)} vs. ${JSON.stringify(change)}`); 45 | // throw new Error(`Concurrent changes at index ${lastIndex}`); 46 | continue; 47 | } 48 | content = content.slice(0, change.span.start) + change.newText + 49 | content.slice(change.span.start + change.span.length); 50 | 51 | lastChange = change; 52 | // console.warn(`Applied change ${JSON.stringify(change)}:\n${content}`); 53 | } 54 | return content; 55 | } 56 | -------------------------------------------------------------------------------- /test/specs/detects string methods.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | function f5(s) { 11 | return s.length == 0 ? '' : s.substring(1); 12 | } 13 | 14 | function ff5(s) { 15 | return s.length == 0 ? '' : s; 16 | } 17 | 18 | function fff5(s) { 19 | return s.substring(1); 20 | } 21 | 22 | function g5(s) { 23 | return s ? s : ''; 24 | } 25 | 26 | function h5(x) { 27 | if (ff5(x) == '' && g5(x) == '') { 28 | console.log('error'); 29 | } 30 | } 31 | 32 | function i5(x1, x2) { 33 | if (x2.y) return x2.y.length; 34 | return x1 ? x1.y : null; 35 | } 36 | 37 | 38 | ` 39 | }, 40 | options: {}, 41 | result: { 42 | files: { 43 | 'input.js': ` 44 | function f5(s: string) { 45 | return s.length == 0 ? '' : s.substring(1); 46 | } 47 | 48 | function ff5(s: string): string { 49 | return s.length == 0 ? '' : s; 50 | } 51 | 52 | function fff5(s: string) { 53 | return s.substring(1); 54 | } 55 | 56 | function g5(s: string): string { 57 | return s ? s : ''; 58 | } 59 | 60 | function h5(x: string): void { 61 | if (ff5(x) == '' && g5(x) == '') { 62 | console.log('error'); 63 | } 64 | } 65 | 66 | function i5(x1?: {readonly y: any}, x2: {readonly y: string}) { 67 | if (x2.y) return x2.y.length; 68 | return x1 ? x1.y : null; 69 | } 70 | 71 | 72 | ` 73 | }, 74 | metadata: { 75 | inferencePasses: 4 76 | } 77 | } 78 | } as TestSpec 79 | -------------------------------------------------------------------------------- /test/specs/resolves imports.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'foo.ts': ` 10 | import {Bar, takeBar} from './bar'; 11 | import * as bar from './bar'; 12 | import * as baz from './baz'; 13 | function foo(x) { 14 | takeBar(x); 15 | takeBar(); 16 | 17 | bar.takeBar(1); 18 | } 19 | foo(new Bar()); 20 | 21 | function babaz(x) { 22 | baz.a(x); 23 | baz.a(1); 24 | 25 | baz.b(''); 26 | 27 | baz.c = ''; 28 | } 29 | `, 30 | 'bar.ts': ` 31 | export class Bar {} 32 | export function takeBar(x) {} 33 | `, 34 | 'baz.ts': ` 35 | export default { 36 | a: a, 37 | b: function(x) {}, 38 | c: 1 39 | } 40 | function a(x) { 41 | x.y.z(); 42 | } 43 | ` 44 | }, 45 | options: {}, 46 | result: { 47 | files: { 48 | 'foo.ts': ` 49 | import {Bar, takeBar} from './bar'; 50 | import * as bar from './bar'; 51 | import * as baz from './baz'; 52 | function foo(x: Bar): void { 53 | takeBar(x); 54 | takeBar(); 55 | 56 | bar.takeBar(1); 57 | } 58 | foo(new Bar()); 59 | 60 | function babaz(x): void { 61 | baz.a(x); 62 | baz.a(1); 63 | 64 | baz.b(''); 65 | 66 | baz.c = ''; 67 | } 68 | `, 69 | 'bar.ts': ` 70 | export class Bar {} 71 | export function takeBar(x): void {} 72 | `, 73 | 'baz.ts': ` 74 | export default { 75 | a: a, 76 | b: function(x): void {}, 77 | c: 1 78 | } 79 | function a(x: {readonly y: {z(): void}}): void { 80 | x.y.z(); 81 | } 82 | ` 83 | }, 84 | metadata: { 85 | inferencePasses: 2 86 | } 87 | } 88 | } as TestSpec 89 | -------------------------------------------------------------------------------- /src/utils/nodes.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | export * from './node_predicates'; 3 | import * as nodes from './node_predicates'; 4 | 5 | export function traverse(root: ts.Node, f: (node: ts.Node) => void): void { 6 | ts.forEachChild(root, visit); 7 | function visit(node: ts.Node) { 8 | f(node); 9 | ts.forEachChild(node, visit); 10 | } 11 | } 12 | 13 | export function isCallTarget(n: ts.Node): boolean { 14 | if (n.parent && nodes.isCallExpression(n.parent)) { 15 | const call = n.parent; 16 | return call.expression === n; 17 | } 18 | return false; 19 | } 20 | 21 | export function findParent( 22 | node: ts.Node, predicate: (parent: ts.Node) => boolean) { 23 | let parent = node.parent; 24 | while (parent) { 25 | if (predicate(parent)) { 26 | return parent; 27 | } 28 | parent = parent.parent; 29 | } 30 | return undefined; 31 | } 32 | 33 | export function getNodeKindDebugDescription(node: ts.Node) { 34 | return Object.keys(ts.SyntaxKind).find(k => ts.SyntaxKind[k] == node.kind); 35 | } 36 | 37 | export function getRequiredPath(node?: ts.Node): string|undefined { 38 | if (node && 39 | nodes.isCallExpression(node) && 40 | nodes.isIdentifier(node.expression) && 41 | node.expression.text == 'require' && 42 | !node.typeArguments && 43 | node.arguments.length == 1) { 44 | const [arg] = node.arguments; 45 | if (nodes.isStringLiteral(arg)) { 46 | return arg.text; 47 | } 48 | } 49 | return undefined; 50 | } 51 | 52 | export function isReadonly(node: ts.Node): boolean { 53 | return hasModifier(node, nodes.isReadonlyKeyword); 54 | } 55 | 56 | export function isStatic(node: ts.Node): boolean { 57 | return hasModifier(node, nodes.isStaticKeyword); 58 | } 59 | 60 | export function hasModifier(node: ts.Node, predicate: (mod: ts.Modifier) => boolean): boolean { 61 | return nodes.isPropertySignature(node) && node.modifiers != null && node.modifiers.some(predicate); 62 | } 63 | 64 | export function isPrototypeAccess(node: ts.Node): node is ts.PropertyAccessExpression { 65 | return nodes.isPropertyAccessExpression(node) && node.name.text === 'prototype'; 66 | } -------------------------------------------------------------------------------- /src/editor/indent.ts: -------------------------------------------------------------------------------- 1 | export function addTabIndentSupport( 2 | textArea: HTMLTextAreaElement, contentChanged: (content: string) => void) { 3 | const tab = ' '; 4 | textArea.addEventListener('keydown', (e) => { 5 | if ((e.keyCode || e.which) == '\t'.charCodeAt(0)) { 6 | e.preventDefault(); 7 | const start = textArea.selectionStart; 8 | const end = textArea.selectionEnd; 9 | const text = textArea.value; 10 | if (start == end && !e.shiftKey) { 11 | textArea.value = text.substring(0, start) + tab + text.substring(end); 12 | textArea.selectionStart = start + tab.length; 13 | textArea.selectionEnd = textArea.selectionStart; 14 | } else { 15 | const selection = text.substring(start, end); 16 | let preTabIndex = text.lastIndexOf('\n', start); 17 | if (preTabIndex < 0) 18 | preTabIndex = 0; 19 | else 20 | preTabIndex++; 21 | 22 | let skippedPreTabIndex = preTabIndex; 23 | let newSelection: string; 24 | let pre: string; 25 | if (e.shiftKey) { 26 | newSelection = selection.replace(new RegExp('\n' + tab, 'g'), '\n'); 27 | if (text.indexOf(tab, preTabIndex) == preTabIndex) { 28 | skippedPreTabIndex = preTabIndex + tab.length; 29 | } 30 | pre = ''; 31 | } else { 32 | newSelection = selection.replace(/\n/g, '\n' + tab); 33 | pre = tab; 34 | } 35 | if (preTabIndex == start) { 36 | textArea.value = pre + 37 | newSelection.substring(skippedPreTabIndex - preTabIndex) + 38 | text.substring(end); 39 | } else { 40 | textArea.value = text.substring(0, preTabIndex) + pre + 41 | text.substring(skippedPreTabIndex, start) + newSelection + 42 | text.substring(end); 43 | } 44 | const startOffset = pre.length + preTabIndex - skippedPreTabIndex; 45 | textArea.selectionStart = start + startOffset; 46 | textArea.selectionEnd = 47 | end + startOffset + newSelection.length - selection.length; 48 | } 49 | contentChanged(textArea.value); 50 | } 51 | }); 52 | } -------------------------------------------------------------------------------- /test/specs/rewrites exports.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'untransformable exports.ts': ` 10 | module.exports = { 11 | f() { return 1 }, 12 | n: 1 13 | }; 14 | 15 | 16 | `, 17 | 'independent exports.ts': ` 18 | module.exports = { 19 | id_foo: id_f, 20 | id_g: id_g, 21 | id_n: id_n, 22 | }; 23 | 24 | var id_n = 1; 25 | 26 | function id_f() {} 27 | function id_g() {} 28 | function id_h() {} 29 | `, 30 | 'exported default value.ts': ` 31 | module.exports = a; 32 | var a = 1; 33 | `, 34 | 'exported default function.ts': ` 35 | module.exports = function(x) {}; 36 | `, 37 | 'exported default aliased function.ts': ` 38 | module.exports = f; 39 | function f(x) {} 40 | `, 41 | 'exported vars in multiple declaration are too complex.ts': ` 42 | module.exports = { 43 | a: a 44 | }; 45 | var a = 1, b = 2; 46 | 47 | 48 | ` 49 | }, 50 | options: {}, 51 | result: { 52 | files: { 53 | 'untransformable exports.ts': ` 54 | export default { 55 | f(): number { return 1 }, 56 | n: 1 57 | }; 58 | 59 | 60 | `, 61 | 'independent exports.ts': ` 62 | 63 | 64 | export var id_n: number = 1; 65 | 66 | function id_f(): void {} 67 | export {id_f as id_foo}; 68 | 69 | export function id_g(): void {} 70 | function id_h(): void {} 71 | `, 72 | 'exported default value.ts': ` 73 | module.exports = a; 74 | var a: number = 1; 75 | `, 76 | 'exported default function.ts': ` 77 | export default function(x) {}; 78 | `, 79 | 'exported default aliased function.ts': ` 80 | 81 | export default function f(x): void {} 82 | `, 83 | 'exported vars in multiple declaration are too complex.ts': ` 84 | export default { 85 | a: a 86 | }; 87 | var a: number = 1, b: number = 2; 88 | 89 | 90 | ` 91 | }, 92 | metadata: { 93 | inferencePasses: 2 94 | } 95 | } 96 | } as TestSpec 97 | -------------------------------------------------------------------------------- /src/passes/declarations.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {ReactorCallback} from '../utils/language_service_reactor'; 3 | import * as nodes from '../utils/nodes'; 4 | import {Mutator} from '../utils/mutator'; 5 | 6 | export const turnToDeclarations: ReactorCallback = (fileNames, services, addChange, _) => { 7 | const program = services.getProgram(); 8 | 9 | for (const sourceFile of program.getSourceFiles()) { 10 | const fileName = sourceFile.fileName; 11 | const mutator = new Mutator(sourceFile.fileName, addChange); 12 | if (fileNames.indexOf(fileName) < 0) { 13 | continue; 14 | } 15 | 16 | ts.forEachChild(sourceFile, visit); 17 | 18 | function removeBody(node: ts.Node&{body?: ts.Node}) { 19 | if (node.body) { 20 | mutator.removeNode(node.body, ';'); 21 | } 22 | } 23 | function removeInitializer( 24 | node: ts.Node&{name: ts.Node, type?: ts.Node, initializer?: ts.Node}) { 25 | if (node.initializer) { 26 | mutator.remove({ 27 | start: node.type ? node.type.getEnd() : node.name.getEnd(), 28 | end: node.initializer.getEnd() 29 | }); 30 | } 31 | } 32 | function remove(node: ts.Node) { 33 | mutator.removeNode(node); 34 | } 35 | 36 | function visit(node: ts.Node) { 37 | if (nodes.isFunctionLikeDeclaration(node)) { 38 | mutator.insert(node.getStart(), 'declare '); 39 | removeBody(node); 40 | } else if (nodes.isClassDeclaration(node)) { 41 | mutator.insert(node.getStart(), 'declare '); 42 | for (const member of node.members) { 43 | if (nodes.isPropertyDeclaration(member)) { 44 | removeInitializer(member); 45 | } else if (nodes.isFunctionLikeDeclaration(member)) { 46 | removeBody(member); 47 | } else { 48 | remove(member); 49 | } 50 | } 51 | } else if ( 52 | nodes.isTypeAliasDeclaration(node) || 53 | nodes.isInterfaceDeclaration(node)) { 54 | // Do nothing. 55 | } else if (nodes.isVariableStatement(node)) { 56 | mutator.insert(node.getStart(), 'declare '); 57 | ts.forEachChild(node, visit); 58 | } else if (nodes.isVariableDeclarationList(node)) { 59 | ts.forEachChild(node, visit); 60 | } else if (nodes.isVariableDeclaration(node)) { 61 | removeInitializer(node); 62 | } else { 63 | remove(node); 64 | } 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import {defaultOptions, Options} from './options'; 5 | import {writeSpec} from './testing/test_spec'; 6 | import {runTyper} from './typer'; 7 | import {getFile, mkdirsSync} from './utils/files'; 8 | import {pseudoJson} from './utils/pseudo_json'; 9 | 10 | require('source-map-support').install(); 11 | 12 | var argv = require('minimist')(process.argv.slice(2)); 13 | // console.dir(argv); 14 | 15 | const force = argv.force || argv.f; 16 | const outputDir = argv.outputDir || argv.o || 'build'; 17 | const outputToStdout = outputDir == '-'; 18 | const testSpecDescription = argv.testSpec; 19 | 20 | const fileNames = argv['_']; 21 | 22 | const inputContents: {[fileName: string]: string} = Object.create(null); 23 | for (const fileName of fileNames) { 24 | if (!fileName.endsWith('.js') && !fileName.endsWith('.ts')) { 25 | console.warn( 26 | `ERROR: file '${fileName}' does not have a JavaScript extension.`); 27 | process.exit(1); 28 | } 29 | const tsFileName = fileName.slice(0, -3) + '.ts'; 30 | inputContents[tsFileName] = fs.readFileSync(fileName).toString(); 31 | } 32 | 33 | const options = 34 | {...defaultOptions, currentWorkingDir: process.cwd(), ...argv}; 35 | const results = runTyper(inputContents, options); 36 | 37 | console.warn(`${results.metadata.inferencePasses} inference passes`); 38 | 39 | if (outputToStdout) { 40 | console.log(pseudoJson(results)); 41 | } else if (testSpecDescription) { 42 | writeSpec(testSpecDescription, { 43 | files: inputContents, 44 | options: options, 45 | result: results 46 | }); 47 | } else { 48 | const filesToWrite: [string, string][] = []; 49 | for (const fileName in results.files) { 50 | const outputFile = getFile( 51 | fileName, 52 | {outputDir: outputDir, currentWorkingDir: options.currentWorkingDir}); 53 | const content = results.files[fileName]; 54 | 55 | if (fs.existsSync(outputFile) && !force) { 56 | console.warn(`${outputFile} already exists. Choose another --outputDir or --force overwrite.`); 57 | process.exit(1); 58 | } 59 | console.warn(`${outputFile}`); 60 | 61 | filesToWrite.push([outputFile, content]); 62 | } 63 | for (const [outputFile, content] of filesToWrite) { 64 | if (!fs.existsSync(outputFile) || 65 | content != fs.readFileSync(outputFile).toString()) { 66 | mkdirsSync(path.dirname(outputFile)); 67 | 68 | fs.writeFileSync(outputFile, content); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/flags.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | function flagsTester(...flagss: ts.TypeFlags[]): (testee: ts.Type|ts.TypeFlags| 4 | undefined) => boolean { 5 | return (testee) => { 6 | if (!testee) return false; 7 | let testeeFlags = typeof testee == 'number' ? testee : testee.flags; 8 | return flagss.some(flags => (testeeFlags & flags) == flags); 9 | } 10 | } 11 | 12 | export const isAny = flagsTester(ts.TypeFlags.Any); 13 | export const isBoolean = 14 | flagsTester(ts.TypeFlags.Boolean, ts.TypeFlags.BooleanLiteral); 15 | export const isBooleanLike = flagsTester(ts.TypeFlags.BooleanLike); 16 | export const isNull = flagsTester(ts.TypeFlags.Null); 17 | export const isNullOrUndefined = 18 | flagsTester(ts.TypeFlags.Null, ts.TypeFlags.Undefined); 19 | export const isNumber = 20 | flagsTester(ts.TypeFlags.Number, ts.TypeFlags.NumberLiteral); 21 | export const isNumberOrString = flagsTester( 22 | ts.TypeFlags.Number, ts.TypeFlags.NumberLiteral, ts.TypeFlags.String, 23 | ts.TypeFlags.StringLiteral, ts.TypeFlags.StringOrNumberLiteral); 24 | export const isObject = flagsTester(ts.TypeFlags.String, ts.TypeFlags.Object); 25 | export const isString = 26 | flagsTester(ts.TypeFlags.String, ts.TypeFlags.StringLiteral); 27 | export const isStructuredType = flagsTester(ts.TypeFlags.StructuredType); 28 | export const isUndefined = flagsTester(ts.TypeFlags.Undefined); 29 | export const isUnion = flagsTester(ts.TypeFlags.Union); 30 | export const isVoid = flagsTester(ts.TypeFlags.Void); 31 | 32 | export const isPrimitive = flagsTester( 33 | ts.TypeFlags.Number, ts.TypeFlags.NumberLiteral, ts.TypeFlags.String, 34 | ts.TypeFlags.StringLiteral, ts.TypeFlags.StringOrNumberLiteral, 35 | ts.TypeFlags.Boolean, ts.TypeFlags.BooleanLiteral); 36 | 37 | export function normalize(flags: ts.TypeFlags) { 38 | function replaceFlag(original: ts.TypeFlags, replacement: ts.TypeFlags) { 39 | if (flags & original) { 40 | flags = (flags & ~original) | replacement; 41 | } 42 | } 43 | replaceFlag(ts.TypeFlags.BooleanLiteral, ts.TypeFlags.Boolean); 44 | replaceFlag(ts.TypeFlags.StringLiteral, ts.TypeFlags.String); 45 | replaceFlag(ts.TypeFlags.NumberLiteral, ts.TypeFlags.Number); 46 | replaceFlag(ts.TypeFlags.EnumLiteral, ts.TypeFlags.Enum); 47 | return flags; 48 | } 49 | 50 | // export function getFlagsDebugDescription(flags: ts.TypeFlags) { 51 | // return Object.keys(ts.SyntaxKind).find(k => ts.SyntaxKind[k] == 52 | // node.kind); 53 | // } -------------------------------------------------------------------------------- /src/testing/test_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import {Options} from '../options'; 4 | import {TyperExecutionResult} from '../typer'; 5 | import {pseudoJson} from '../utils/pseudo_json'; 6 | 7 | export declare interface TestSpec { 8 | files: {[fileName: string]: string}, 9 | options: Partial, 10 | result: TyperExecutionResult 11 | } 12 | 13 | // function deindentSpec(spec: TestSpec): TestSpec { 14 | // if (!spec.files) throw new Error(`O FILES in ${JSON.stringify(spec)}`); 15 | // if (!spec.result.files) throw new Error(`O RESULT FILES in ${JSON.stringify(spec)}`); 16 | // return { 17 | // files: mapValues(spec.files, deindent), 18 | // options: spec.options, 19 | // result: { 20 | // files: mapValues(spec.result.files, deindent), 21 | // metadata: spec.result.metadata 22 | // } 23 | // } 24 | // } 25 | 26 | function isTestSpec(obj: any): obj is TestSpec { 27 | return obj && 'files' in obj && 'options' in obj && 'result' in obj && 'files' in obj.result && 'metadata' in obj.result; 28 | } 29 | // function isOldTestSpec(obj: any): obj is OldTestSpec { 30 | // return 'inputs' in obj && 'outputs' in obj && 'metadata' in obj; 31 | // } 32 | export function readSpec(fileName: string): TestSpec { 33 | const mod = module.require(fileName); 34 | let spec = mod['default']; 35 | // if (isOldTestSpec(spec)) { 36 | // spec = { 37 | // files: spec.inputs, 38 | // options: Object.create(defaultOptions), 39 | // result: { 40 | // files: spec.outputs, 41 | // metadata: spec.metadata 42 | // } 43 | // } 44 | // } 45 | if (!isTestSpec(spec)) { 46 | throw new Error(`Bad spec format: ${JSON.stringify(Object.keys(spec), null, 2)}`); 47 | } 48 | 49 | return spec; 50 | // const deindented = deindentSpec(spec); 51 | // return deindented; 52 | } 53 | 54 | export async function writeSpec(fileName: string, spec: TestSpec) { 55 | spec = {...spec}; 56 | const src = `// SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS.\n` + 57 | `//\n` + 58 | `// REGENERATE OUTPUTS AND METADATA WITH \`npm run update-specs\`.\n` + 59 | `\n` + 60 | `import {TestSpec} from '../../src/testing/test_spec';\n` + 61 | `\n` + 62 | `export default ${pseudoJson(spec)} as TestSpec\n`; 63 | 64 | await new Promise((resolve, reject) => {fs.writeFile(fileName, src, err => { 65 | if (err) 66 | reject(err); 67 | else 68 | resolve(); 69 | })}); 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstyper", 3 | "version": "0.5.0", 4 | "author": { 5 | "name": "Olivier Chafik", 6 | "email": "olivier.chafik@gmail.com", 7 | "url": "http://ochafik.com" 8 | }, 9 | "license": "ISC", 10 | "bugs": { 11 | "url": "https://github.com/ochafik/jstyper/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ochafik/jstyper.git" 16 | }, 17 | "categories": [ 18 | "Linters", 19 | "Formatters", 20 | "Languages" 21 | ], 22 | "keywords": [ 23 | "typescript", 24 | "flow", 25 | "closure", 26 | "types", 27 | "migration" 28 | ], 29 | "devDependencies": { 30 | "@types/chai": "^3.4.34", 31 | "@types/minimist": "^1.2.0", 32 | "@types/mocha": "^2.2.39", 33 | "@types/node": "^7.0.4", 34 | "babel-plugin-external-helpers": "^6.22.0", 35 | "babel-preset-es2015": "^6.22.0", 36 | "chai": "^3.5.0", 37 | "clang-format": "^1.0.46", 38 | "concurrently": "^3.1.0", 39 | "inline-assets": "^1.1.2", 40 | "lite-server": "^2.2.2", 41 | "mocha": "^3.2.0", 42 | "rollup": "^0.41.4", 43 | "rollup-plugin-babel": "^2.7.1", 44 | "rollup-plugin-commonjs": "^7.0.0", 45 | "rollup-plugin-filesize": "^1.0.1", 46 | "rollup-plugin-node-resolve": "^2.0.0", 47 | "rollup-plugin-typescript": "^0.8.1", 48 | "rollup-plugin-uglify": "^1.0.1", 49 | "rollup-watch": "^3.2.2", 50 | "typescript": "^2.2.0", 51 | "uglify-js-harmony": "^2.6.2" 52 | }, 53 | "dependencies": { 54 | "minimist": "^1.2.0", 55 | "source-map-support": "^0.4.11", 56 | "tslib": "^1.5.0" 57 | }, 58 | "scripts": { 59 | "build": "rm -fR build && npm run rollup:demo && npm run rollup:cli && inline-assets index.html build/demo.html", 60 | "demo": "concurrently --kill-others \"DEV=1 npm run rollup:demo -- -w\" lite-server", 61 | "start": "concurrently --kill-others \"npm run demo\" \"npm run test:w\"", 62 | "rollup:demo": "rollup -c rollup.config.demo.js", 63 | "rollup:cli": "rollup -c rollup.config.cli.js", 64 | "test": "tsc && mocha build/test", 65 | "test:w": "tsc && concurrently \"tsc -w\" \"mocha build/test --watch\"", 66 | "update-specs": "UPDATE_SPECS=1 npm test", 67 | "update-specs:w": "UPDATE_SPECS=1 npm run test:w", 68 | "format": "clang-format --glob=src/**/*.ts -i -style=Google", 69 | "prepare-release": "npm test && npm run build", 70 | "major-release": "npm run prepare-release && npm version major && npm publish && git push --follow-tags && ./deploy.sh", 71 | "minor-release": "npm run prepare-release && npm version minor && npm publish && git push --follow-tags && ./deploy.sh", 72 | "patch-release": "npm run prepare-release && npm version patch && npm publish && git push --follow-tags && ./deploy.sh" 73 | }, 74 | "bin": { 75 | "jstyper": "bin/jstyper.js" 76 | }, 77 | "files": [ 78 | "bin/jstyper.js", 79 | "build/cli.js", 80 | "build/demo.html", 81 | "build/demo.js.map" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 71 | 72 | 73 | 74 | 85 | 86 | 87 |
88 | 89 |
90 |
91 | Max passes: 92 | 93 |
94 |
95 | Auto-run 96 | 97 |
98 |
99 |
100 |
101 | 102 |
103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /src/typer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {defaultOptions} from './options'; 4 | import {turnToDeclarations} from './passes/declarations'; 5 | import {format} from './passes/format'; 6 | import {infer} from './passes/infer'; 7 | import {updateExports} from './passes/update_exports'; 8 | import {updateImports} from './passes/update_imports'; 9 | import {updateVars} from './passes/update_vars'; 10 | import {LanguageServiceReactor} from './utils/language_service_reactor'; 11 | import {mapKeys} from './utils/maps'; 12 | 13 | export interface TyperExecutionMetadata { inferencePasses: number, } 14 | export interface TyperExecutionResult { 15 | files: {[fileName: string]: string}, // Map, 16 | metadata: TyperExecutionMetadata, 17 | } 18 | 19 | export function runTyper( 20 | fileContents: {[fileName: string]: string}, 21 | options = defaultOptions): TyperExecutionResult { 22 | 23 | // fileContents = { 24 | // ...fileContents, 25 | // 'foo.ts': ` 26 | // export function fooFoo(x) {} 27 | // ` 28 | // // 'node_modules/typescript/lib/lib.dom.d.ts': ` 29 | // // declare var document: { 30 | // // getElementById(id: string): number; 31 | // // }; 32 | // // ` 33 | // } 34 | const reactor = 35 | new LanguageServiceReactor(fileContents, options.currentWorkingDir, { 36 | target: ts.ScriptTarget.ES2017, 37 | module: ts.ModuleKind.ES2015, 38 | moduleResolution: ts.ModuleResolutionKind.NodeJs, 39 | //lib: ["dom", "es2017"], 40 | allowJs: true, 41 | strictNullChecks: true, 42 | removeComments: true, 43 | } as ts.CompilerOptions); 44 | 45 | if (options.updateImports) reactor.react(updateImports); 46 | if (options.updateExports) reactor.react(updateExports); 47 | 48 | const inferrer = infer(options); 49 | let inferencePasses = 0; 50 | for (let i = 0; i < options.maxIterations; i++) { 51 | if (options.debugPasses) { 52 | console.warn( 53 | `Running incremental type inference (${i + 54 | 1} / ${options.maxIterations})...`); 55 | } 56 | inferencePasses++; 57 | if (!reactor.react(inferrer)) { 58 | break; 59 | } 60 | if (options.debugPasses) { 61 | console.warn(`Partial result after inference pass ${i + 1}:`); 62 | const fileContents = reactor.fileContents; 63 | for (const fileName in fileContents) { 64 | const contents = fileContents[fileName]; 65 | console.warn(`${fileName}:\n${contents}\n`); 66 | } 67 | } 68 | } 69 | 70 | const metadata = { 71 | inferencePasses: inferencePasses 72 | } 73 | 74 | if (options.format) reactor.react(format); 75 | if (options.updateVars) reactor.react(updateVars); 76 | if (options.declarations) { 77 | reactor.react(turnToDeclarations); 78 | 79 | return { 80 | files: mapKeys(reactor.fileContents, k => k.replace(/.[tj]s/, '.d.ts')), 81 | metadata 82 | }; 83 | } 84 | 85 | return { 86 | files: reactor.fileContents, metadata 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/editor/undo.ts: -------------------------------------------------------------------------------- 1 | export function addUndoSupport( 2 | textArea: HTMLTextAreaElement, 3 | contentChanged: (content: string) => void): UndoManager { 4 | const manager = new UndoManager(textArea.value); 5 | textArea.addEventListener('input', () => { 6 | manager.content = textArea.value; 7 | }); 8 | textArea.addEventListener('keydown', (e) => { 9 | // console.log(`KEYDOWN e.charCode = ${e.charCode}, ${e.keyCode} (e.metaKey 10 | // = ${e.metaKey}, e.ctrlKey = ${e.ctrlKey})`); 11 | if ((e.ctrlKey || e.metaKey) && e.keyCode == 90) { 12 | e.preventDefault(); 13 | if (e.shiftKey) { 14 | manager.redo(); 15 | } else { 16 | manager.undo(); 17 | } 18 | textArea.value = manager.content; 19 | contentChanged(textArea.value); 20 | } 21 | }); 22 | return manager; 23 | } 24 | 25 | type Edit = { 26 | offset: number, 27 | previousText: string, 28 | nextText: string, 29 | }; 30 | 31 | export class UndoManager { 32 | private edits: Edit[] = []; 33 | private editIndex: number = 0; 34 | 35 | constructor(private _content = '') {} 36 | 37 | get content() { 38 | return this._content; 39 | } 40 | set content(newContent: string) { 41 | if (newContent == this._content) return; 42 | this.addEdit(inferDiff(this._content, newContent)); 43 | } 44 | addEdit(edit: Edit) { 45 | // console.log(`EDIT: ${JSON.stringify(edit, null, 2)}`); 46 | this.applyEdit(edit); 47 | this.edits.splice(this.editIndex, this.edits.length - this.editIndex, edit); 48 | this.editIndex++; 49 | } 50 | undo() { 51 | if (this.editIndex == 0) return; 52 | 53 | const edit = this.edits[--this.editIndex]; 54 | // console.log(`UNDO: original edit: ${JSON.stringify(edit, null, 2)}`); 55 | this.applyEdit(invertEdit(edit)); 56 | } 57 | redo() { 58 | if (this.editIndex == this.edits.length) return; 59 | 60 | const edit = this.edits[this.editIndex++]; 61 | this.applyEdit(edit); 62 | } 63 | 64 | applyEdit(edit: Edit) { 65 | // console.log(`APPLYING EDIT: ${JSON.stringify(edit, null, 2)}`); 66 | this._content = this._content.substring(0, edit.offset) + edit.nextText + 67 | this._content.substring(edit.offset + edit.previousText.length); 68 | } 69 | } 70 | 71 | function invertEdit(diff: Edit) { 72 | return { 73 | offset: diff.offset, previousText: diff.nextText, 74 | nextText: diff.previousText 75 | } 76 | } 77 | function inferDiff(a: string, b: string): Edit { 78 | let diffStart = 0; 79 | let diffEnd = a.length; 80 | for (let i = 0, n = a.length; i < n; i++) { 81 | if (a[i] == b[i]) { 82 | diffStart = i + 1; 83 | } else { 84 | break; 85 | } 86 | } 87 | for (let i = a.length - 1, j = b.length - 1; 88 | i >= 0 && i >= diffStart && j >= 0; i--, j--) { 89 | if (a[i] == b[j]) { 90 | diffEnd = i; 91 | } else { 92 | break; 93 | } 94 | } 95 | return { 96 | offset: diffStart, 97 | previousText: a.substring(diffStart, diffEnd), 98 | nextText: b.substring(diffStart, b.length - (a.length - diffEnd)) 99 | }; 100 | } -------------------------------------------------------------------------------- /test/specs/recognizes Object.defineProperty.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'returned or assigned.js': ` 10 | function f_ret() { 11 | return Object.defineProperty({}, 'a', { value: 1 }); 12 | } 13 | let x_assigned = Object.defineProperty({}, 'a', { value: 1 }); 14 | `, 15 | 'readonlies.js': ` 16 | function f(x) { 17 | Object.defineProperty(x, 'a', { 18 | value: 1 19 | }); 20 | Object.defineProperty(x, 'b', { 21 | value: 1, 22 | writable: false 23 | }); 24 | Object.defineProperty(x, 'c', { 25 | get: () => ({x: 1}), 26 | }); 27 | } 28 | `, 29 | 'writables.js': ` 30 | function g(x) { 31 | Object.defineProperty(x, 'a', { 32 | value: '', 33 | writable: true 34 | }); 35 | Object.defineProperty(x, 'b', { 36 | get: () => ({x: 1}), 37 | set: (v) => {} 38 | }); 39 | } 40 | `, 41 | 'properties.js': ` 42 | function h(x) { 43 | Object.defineProperties(x, { 44 | 'a': { 45 | value: 1 46 | }, 47 | b: { 48 | writable: false 49 | } 50 | }); 51 | } 52 | ` 53 | }, 54 | options: { 55 | differentiateComputedProperties: true 56 | }, 57 | result: { 58 | files: { 59 | 'returned or assigned.js': ` 60 | function f_ret(): {readonly ['a']?: number} { 61 | return Object.defineProperty({}, 'a', { value: 1 }); 62 | } 63 | let x_assigned: {readonly ['a']?: number} = Object.defineProperty({}, 'a', { value: 1 }); 64 | `, 65 | 'readonlies.js': ` 66 | function f(x: {readonly ['a']?: number, readonly ['b']?: number, readonly ['c']?: {x: number}}): void { 67 | Object.defineProperty(x, 'a', { 68 | value: 1 69 | }); 70 | Object.defineProperty(x, 'b', { 71 | value: 1, 72 | writable: false 73 | }); 74 | Object.defineProperty(x, 'c', { 75 | get: () => ({x: 1}), 76 | }); 77 | } 78 | `, 79 | 'writables.js': ` 80 | function g(x: {['a']?: string, ['b']?: {x: number}}): void { 81 | Object.defineProperty(x, 'a', { 82 | value: '', 83 | writable: true 84 | }); 85 | Object.defineProperty(x, 'b', { 86 | get: () => ({x: 1}), 87 | set: (v) => {} 88 | }); 89 | } 90 | `, 91 | 'properties.js': ` 92 | function h(x: {readonly b: any, readonly ['a']?: number}): void { 93 | Object.defineProperties(x, { 94 | 'a': { 95 | value: 1 96 | }, 97 | b: { 98 | writable: false 99 | } 100 | }); 101 | } 102 | ` 103 | }, 104 | metadata: { 105 | inferencePasses: 2 106 | } 107 | } 108 | } as TestSpec 109 | -------------------------------------------------------------------------------- /src/utils/type_predicates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function isUnion(type?: ts.Type): type is ts.UnionType { 4 | return type != null && (type.flags & ts.TypeFlags.Union) != 0; 5 | } 6 | 7 | export function isIntersection(type?: ts.Type): type is ts.IntersectionType { 8 | return type != null && (type.flags & ts.TypeFlags.Intersection) != 0; 9 | } 10 | 11 | export function isLiteral(type?: ts.Type): type is ts.LiteralType { 12 | return type != null && (type.flags & ts.TypeFlags.Literal) != 0; 13 | } 14 | 15 | export function isEnum(type?: ts.Type): type is ts.EnumType { 16 | return type != null && (type.flags & ts.TypeFlags.Enum) != 0; 17 | } 18 | 19 | export function isEnumLiteral(type?: ts.Type): type is ts.EnumLiteralType { 20 | return type != null && (type.flags & ts.TypeFlags.EnumLiteral) != 0; 21 | } 22 | 23 | export function isObject(type?: ts.Type): type is ts.ObjectType { 24 | return type != null && (type.flags & ts.TypeFlags.Object) != 0; 25 | } 26 | 27 | export function isClass(type?: ts.Type): type is ts.ObjectType { 28 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Class) != 0; 29 | } 30 | export function isInterface(type?: ts.Type): type is ts.ObjectType { 31 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Interface) != 0; 32 | } 33 | 34 | export function isReference(type?: ts.Type): type is ts.ObjectType { 35 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Reference) != 0; 36 | } 37 | export function isTuple(type?: ts.Type): type is ts.ObjectType { 38 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Tuple) != 0; 39 | } 40 | export function isAnonymous(type?: ts.Type): type is ts.ObjectType { 41 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Anonymous) != 0; 42 | } 43 | export function isMapped(type?: ts.Type): type is ts.ObjectType { 44 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Mapped) != 0; 45 | } 46 | export function isInstantiated(type?: ts.Type): type is ts.ObjectType { 47 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.Instantiated) != 0; 48 | } 49 | export function isObjectLiteral(type?: ts.Type): type is ts.ObjectType { 50 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.ObjectLiteral) != 0; 51 | } 52 | export function isEvolvingArray(type?: ts.Type): type is ts.ObjectType { 53 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.EvolvingArray) != 0; 54 | } 55 | export function isObjectLiteralPatternWithComputedProperties(type?: ts.Type): type is ts.ObjectType { 56 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties) != 0; 57 | } 58 | export function isNonPrimitive(type?: ts.Type): type is ts.ObjectType { 59 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.NonPrimitive) != 0; 60 | } 61 | export function isClassOrInterface(type?: ts.Type): type is ts.ObjectType { 62 | return isObject(type) && (type.objectFlags & ts.ObjectFlags.ClassOrInterface) != 0; 63 | } 64 | 65 | // export function isClassOrInterface(type?: ts.Type): type is ts.InterfaceType { 66 | // return type != null && (type.flags & (ts.TypeFlags.Class | ts.TypeFlags.Interface)) != 0; 67 | // } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jstyper [![Build Status](https://travis-ci.org/ochafik/jstyper.svg?branch=master)](https://travis-ci.org/ochafik/jstyper) [![npm version](https://badge.fury.io/js/jstyper.svg)](https://badge.fury.io/js/jstyper) 2 | Turns your JavaScript to TypeScript / Flow (adds types, converts requires to imports, etc); *ALPHA QUALITY* 3 | 4 | JsTyper adds {TypeScript, Flow, Closure} types to JavaScript programs using iterative type propagation and the TypeScript Language Services. 5 | 6 | # Run it 7 | 8 | * [Interactive online demo](http://ochafik.com/assets/typer-demo.html) 9 | * `npm i -g jstyper` then `jstyper input.js` 10 | * Noteworthy flags: 11 | * `-o ` (or `--outputDir=`): where files should be generated 12 | * `--declarations`: only generate interface files (`*.d.ts`) 13 | 14 | # Features 15 | 16 | (take a look at [our tests](https://github.com/ochafik/jstyper/tree/master/test/specs)) 17 | 18 | - Assumes your JS code is correct, and propagates inferred types globally (across different files): 19 | - From call sites: if you call `f(1)`, it will assume `function f` can take a `number` argument. If you call `f()` somewhere else, it will assume that argument is optional. 20 | - From function bodies: in `function f(x) { return x * 2 }` it's obvious `x` must be a `number`. Similar constraints appear when you test arguments for nullity or for equality with other values, etc. 21 | - From call shapes: from `x.y() * x.n;` it infers `x: {y(): void, readonly n: number}` 22 | - Rewrites `require` & `module.exports` to ES2015 (ES6) imports & exports 23 | - Supports writing declarations (`*.d.ts`) files only with the `--declarations` flag. 24 | - Writes definitions for imported / required module interfaces (e.g. if you `var foo = require('foo'); foo.bar()` it will infer `declare module "foo" { export function bar(): void }`) 25 | - Rewrites `var` to `let` 26 | 27 | # Example 28 | 29 | example.js: 30 | 31 | ```js 32 | function f(x) { 33 | return x * 2; 34 | } 35 | 36 | function g(x, o) { 37 | if (o.addValue) { 38 | return f(x) + o.value; 39 | } 40 | return o.name == 'default' ? x : 'y'; 41 | } 42 | 43 | function gg(x, y) { 44 | var v = g(x, y); 45 | return v; 46 | } 47 | ``` 48 | 49 | example.ts: 50 | 51 | ```js 52 | function f(x: number): number { 53 | return x * 2; 54 | } 55 | 56 | function g(x: number, o: {addValue: boolean, value: any, name: string}) { 57 | if (o.addValue) { 58 | return f(x) + o.value; 59 | } 60 | return o.name == 'default' ? x : 'y'; 61 | } 62 | 63 | function gg(x: number, y: {addValue: boolean, value: any, name: string}) { 64 | let v = g(x, y); 65 | return v; 66 | } 67 | ``` 68 | 69 | # TODO 70 | 71 | - Bundle & support DOM & ES2015+ libs 72 | - Support `var foo = require('./bar').foo` pattern 73 | - Support `var Foo = function() {}; Foo.prototype.bar = ...` (create companion `interface Foo { bar... }`) 74 | - Parse and output flow comment / flow types (+ compare with Flow) 75 | - Lint mode 76 | - Split large tests into smaller specs 77 | - Better propagate contextual types in expressions (e.g. in `(a && b && c`, `a ? b : c`) 78 | - Use type constraints instead of types to allow local inference passes 79 | - Support merging of symbols added after structural members `{x: number} | Foo` 80 | - Handle index operators 81 | - Infer `x: Promise` from `(await x): T` 82 | - WebDriver test for demo 83 | - Support literal types 84 | - Use `const` when appropriate in `var` rewrite. 85 | - Proper cli args parsing + `--help` 86 | - Better `new F()` class creation inference 87 | 88 | # Hack it 89 | 90 | - Clone this repo 91 | - Run `npm i` 92 | - Debug the demo `npm start` (will auto-reload and auto-run tests after any changes made to the TypeScript sources) 93 | - More options: take a look at [package.json's script entries](./package.json) 94 | -------------------------------------------------------------------------------- /src/matchers/objects.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as nodes from '../utils/nodes'; 3 | 4 | export type MatchedPropertyDescriptor = { 5 | writable: boolean, 6 | valueTypes: ts.Type[] 7 | }; 8 | 9 | type MatchedObjectLiteralElement = { 10 | name: string, 11 | isNameComputed: boolean, 12 | value: ts.Node 13 | } 14 | 15 | type MatchedPropertyDescriptors = { 16 | properties: (MatchedPropertyDescriptor & {name: string, isNameComputed: boolean})[] 17 | }; 18 | 19 | export type MatchedDeclarationName = {name: string, isNameComputed: boolean}; 20 | export function matchDeclarationName(node?: ts.DeclarationName): MatchedDeclarationName | undefined { 21 | if (!node) return undefined; 22 | 23 | let name: string; 24 | let isNameComputed: boolean; 25 | if (nodes.isIdentifier(node)) { 26 | name = node.text; 27 | isNameComputed = false; 28 | } else if (nodes.isStringLiteral(node)) { 29 | name = node.text; 30 | isNameComputed = true; 31 | } else if (nodes.isComputedPropertyName(node) && nodes.isStringLiteral(node.expression)) { 32 | name = node.expression.text; 33 | isNameComputed = true; 34 | } else { 35 | return undefined; 36 | } 37 | return {name, isNameComputed}; 38 | } 39 | 40 | export function getKeyValue(node: ts.ObjectLiteralElement): MatchedObjectLiteralElement | undefined { 41 | let value: ts.Node; 42 | if (nodes.isShorthandPropertyAssignment(node)) { 43 | value = node.objectAssignmentInitializer || node.name; 44 | } else if (nodes.isPropertyAssignment(node)) { 45 | value = node.initializer; 46 | } else { 47 | return undefined; 48 | } 49 | 50 | const matchedName = matchDeclarationName(node.name); 51 | return matchedName && { ...matchedName, value }; 52 | } 53 | 54 | export function matchPropertyDescriptors(descs: ts.ObjectLiteralExpression, checker: ts.TypeChecker): MatchedPropertyDescriptors | undefined { 55 | if (nodes.isObjectLiteralExpression(descs)) { 56 | const ret: MatchedPropertyDescriptors = { 57 | properties: [] 58 | }; 59 | for (const prop of descs.properties) { 60 | const kv = getKeyValue(prop); 61 | if (!kv) continue; 62 | 63 | const {name, isNameComputed, value} = kv; 64 | if (nodes.isObjectLiteralExpression(value)) { 65 | ret.properties.push({ 66 | name, 67 | isNameComputed, 68 | ...matchPropertyDescriptor(value, checker) 69 | }); 70 | } 71 | } 72 | return ret; 73 | } 74 | return undefined; 75 | } 76 | 77 | type MatchedSimpleSelect = { 78 | targetName: string, 79 | selectedName: string 80 | }; 81 | 82 | export function matchSimpleSelect(node: ts.Node): MatchedSimpleSelect | undefined { 83 | if (nodes.isPropertyAccessExpression(node) && nodes.isIdentifier(node.expression)) { 84 | const targetName = node.expression.text; 85 | const selectedName = node.name.text; 86 | return {targetName, selectedName}; 87 | } 88 | return undefined; 89 | } 90 | 91 | export function matchPropertyDescriptor(desc: ts.ObjectLiteralExpression, checker: ts.TypeChecker): MatchedPropertyDescriptor { 92 | let writable = false; 93 | const valueTypes: ts.Type[] = []; 94 | 95 | for (const prop of desc.properties) { 96 | const kv = getKeyValue(prop); 97 | if (!kv) continue; 98 | 99 | const {name, value} = kv; 100 | switch (name) { 101 | case 'writable': 102 | writable = !nodes.isFalseKeyword(value); 103 | break; 104 | case 'get': 105 | const getterType = value && checker.getTypeAtLocation(value); 106 | if (getterType) { 107 | for (const sig of getterType.getCallSignatures()) { 108 | valueTypes.push(sig.getReturnType()); 109 | } 110 | } 111 | break; 112 | case 'set': 113 | writable = true; 114 | break; 115 | case 'value': 116 | const valueType = value && checker.getTypeAtLocation(value); 117 | if (valueType) { 118 | valueTypes.push(valueType); 119 | } 120 | break; 121 | } 122 | } 123 | 124 | return { 125 | writable, 126 | valueTypes, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/utils/call_constraints.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as fl from './flags'; 3 | import * as nodes from './nodes'; 4 | import {guessName} from './name_guesser'; 5 | import {TypeConstraints, TypeOpts, getTypeOptions} from './type_constraints'; 6 | 7 | export type Signature = { 8 | returnType?: ts.Type, 9 | argTypes: ts.Type[] 10 | }; 11 | 12 | export class CallConstraints { 13 | private minArity: number|undefined; 14 | private _constructible = false; 15 | private _hasChanges = false; 16 | private _thisConstraints?: TypeConstraints; 17 | 18 | constructor( 19 | private checker: ts.TypeChecker, 20 | private calleeDescription: string, 21 | private createConstraints: (description: string) => TypeConstraints, 22 | public readonly returnType: 23 | TypeConstraints = createConstraints(`${calleeDescription}.return`), 24 | public readonly argTypes: TypeConstraints[] = []) {} 25 | 26 | get hasChanges() { 27 | return this._hasChanges || this.returnType.hasChanges || this.argTypes.some(c => c.hasChanges); 28 | } 29 | 30 | get constructible() { 31 | return this._constructible; 32 | } 33 | 34 | isConstructible(opts?: TypeOpts) { 35 | // markChanges = false 36 | const typeOptions = getTypeOptions(opts); 37 | 38 | if (this._constructible) return; 39 | this._constructible = true; 40 | if (typeOptions.markChanges) { 41 | this._hasChanges = true; 42 | } 43 | } 44 | 45 | hasSignature(sig: ts.Signature, opts?: TypeOpts) { 46 | const decl = sig.getDeclaration(); 47 | if (!decl) { 48 | return; 49 | } 50 | 51 | const params: ts.ParameterDeclaration[] = []; 52 | const paramTypes: ts.Type[] = []; 53 | 54 | decl.parameters.forEach((param, i) => { 55 | const paramType = this.checker.getTypeAtLocation(param); 56 | if (i == 0 && nodes.isIdentifier(param.name) && param.name.text == 'this') {//} nodes.isThisType(param)) { 57 | this.getThisType().isType(paramType); 58 | } else { 59 | params.push(param); 60 | paramTypes.push(paramType); 61 | } 62 | }); 63 | 64 | this.returnType.isType(sig.getReturnType(), opts); 65 | 66 | let minArity = 0; 67 | for (let paramType of paramTypes) { 68 | if (fl.isUndefined(paramType.flags)) { 69 | break; 70 | } 71 | minArity++; 72 | } 73 | this.hasArity(minArity, opts); 74 | 75 | paramTypes.forEach((paramType, i) => { 76 | const param = params[i]; 77 | const paramConstraints = this.getArgType(i, opts); 78 | paramConstraints.isType(paramType, opts); 79 | paramConstraints.addName(guessName(param.name)); 80 | }); 81 | } 82 | 83 | private ensureArity(arity: number, opts?: TypeOpts) { 84 | let updated = false; 85 | for (let i = 0; i < arity; i++) { 86 | if (this.argTypes.length <= i) { 87 | this.argTypes.push(this.createConstraints( 88 | `${this.calleeDescription}.arguments[${i}]`)); 89 | updated = true; 90 | } 91 | } 92 | 93 | if (updated) { 94 | this.updateOptionalArgs(opts); 95 | } 96 | } 97 | hasArity(arity: number, opts?: TypeOpts) { 98 | // this.arities.push(arity); 99 | if (typeof this.minArity === 'undefined' || arity < this.minArity) { 100 | this.minArity = arity; 101 | this.updateOptionalArgs(opts); 102 | } 103 | } 104 | 105 | hasThisType(): boolean { 106 | return this._thisConstraints != null; 107 | } 108 | 109 | getThisType(): TypeConstraints { 110 | if (!this._thisConstraints) { 111 | this._thisConstraints = this.createConstraints('this'); 112 | } 113 | return this._thisConstraints; 114 | } 115 | 116 | getArgType(index: number, opts?: TypeOpts) { 117 | this.ensureArity(index + 1, opts); 118 | return this.argTypes[index]; 119 | } 120 | private updateOptionalArgs(opts?: TypeOpts) { 121 | if (typeof this.minArity === 'undefined') { 122 | return; 123 | } 124 | this.argTypes.forEach((t, i) => { 125 | if (typeof this.minArity == 'number' && i >= this.minArity) { 126 | t.isUndefined(opts); 127 | } 128 | }); 129 | } 130 | }; -------------------------------------------------------------------------------- /src/utils/language_service_reactor.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {VersionedFile} from './versioned_file'; 4 | 5 | export type AddChangeCallback = (fileName: string, change: ts.TextChange) => 6 | void; 7 | export type AddRequirementCallback = (moduleName: string, decls: string) => void 8 | export type ReactorCallback = 9 | (fileNames: string[], services: ts.LanguageService, 10 | addChange: AddChangeCallback, 11 | addRequirement: AddRequirementCallback) => void; 12 | 13 | export class LanguageServiceReactor implements ts.LanguageServiceHost { 14 | private files = new Map(); 15 | private services: ts.LanguageService; 16 | private fileNames: string[]; 17 | 18 | constructor( 19 | fileContents: {[fileName: string]: string}, 20 | private currentWorkingDir = '.', 21 | // private dependenciesFileName: string, 22 | private options: ts.CompilerOptions) { 23 | const fileNames: string[] = []; 24 | for (const fileName in fileContents) { 25 | const content = fileContents[fileName]; 26 | this.files.set(fileName, new VersionedFile(content)); 27 | fileNames.push(fileName); 28 | } 29 | this.services = ts.createLanguageService(this, ts.createDocumentRegistry()); 30 | this.fileNames = fileNames; 31 | } 32 | 33 | getScriptFileNames() { 34 | return this.fileNames; 35 | } 36 | getScriptVersion(fileName) { 37 | const file = this.files.get(fileName); 38 | if (file == null) return ''; 39 | return file.version.toString(); 40 | } 41 | getScriptSnapshot(fileName) { 42 | const file = this.files.get(fileName); 43 | return file && ts.ScriptSnapshot.fromString(file.content); 44 | } 45 | getCurrentDirectory() { 46 | return this.currentWorkingDir; 47 | } 48 | getCompilationSettings() { 49 | return this.options; 50 | } 51 | getDefaultLibFileName(options) { 52 | try { 53 | const result = ts.getDefaultLibFilePath(options); 54 | return result; 55 | } catch (e) { 56 | return 'index.ts'; 57 | } 58 | } 59 | 60 | get fileContents(): {[fileName: string]: string} { 61 | const result = Object.create(null); 62 | for (const [fileName, file] of this.files) { 63 | result[fileName] = file.content; 64 | } 65 | return result; 66 | } 67 | 68 | react(callback: ReactorCallback): boolean { 69 | let changed = false; 70 | const pendingChanges = new Map(); 71 | // const dependenciesFileName = this.options.dependenciesFileName; 72 | // const dependencies: string[] = []; 73 | 74 | const addChange: AddChangeCallback = (fileName, change) => { 75 | const changes = pendingChanges.get(fileName); 76 | if (changes) { 77 | changes.push(change); 78 | } else { 79 | pendingChanges.set(fileName, [change]); 80 | } 81 | }; 82 | 83 | const getFile = (fileName: string) => { 84 | let file = this.files.get(fileName); 85 | if (!file) { 86 | // if (this.fileNames.indexOf(fileName) < 0) { 87 | // this.fileNames.push(fileName); 88 | // } 89 | console.warn(`WARNING: Creating file ${fileName} (existing files: ${this.fileNames})`); 90 | file = new VersionedFile(''); 91 | this.files.set(fileName, file); 92 | } 93 | return file; 94 | }; 95 | 96 | callback(this.fileNames, this.services, addChange, (moduleName, decls) => { 97 | // dependencies.push(decls); 98 | const moduleFileName = `node_modules/@types/${moduleName}/index.d.ts`; 99 | let file = getFile(moduleFileName); 100 | const oldVersion = file.version; 101 | file.content = decls; 102 | if (file.version != oldVersion) { 103 | changed = true; 104 | } 105 | }); 106 | 107 | // if (dependencies.length > 0) { 108 | // let dependenciesFile = getFile(this.dependenciesFileName); 109 | // const oldVersion = dependenciesFile.version; 110 | // dependenciesFile.content = dependencies.join('\n\n'); 111 | // if (dependenciesFile.version != oldVersion) { 112 | // changed = true; 113 | // } 114 | // } 115 | 116 | for (const [fileName, changes] of pendingChanges) { 117 | if (getFile(fileName).commitChanges(changes)) { 118 | changed = true; 119 | } 120 | } 121 | return changed; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/demo.ts: -------------------------------------------------------------------------------- 1 | import {addTabIndentSupport} from './editor/indent'; 2 | import {addUndoSupport} from './editor/undo'; 3 | import {defaultOptions, Options} from './options'; 4 | import {runTyper} from './typer'; 5 | 6 | type State = { 7 | content?: string, 8 | maxIterations?: number, 9 | autoRun?: boolean, 10 | }; 11 | 12 | const defaultState: State = { 13 | content: ` 14 | function f(x, opts) { 15 | return opts && opts.mult ? x * 2 : x / 2; 16 | } 17 | 18 | function addProp(o) { 19 | Object.defineProperty(o, 'x' { 20 | value: 1, 21 | writable: true 22 | }); 23 | } 24 | `.trim(), 25 | maxIterations: defaultOptions.maxIterations, 26 | autoRun: true, 27 | }; 28 | 29 | function readStateFromFragment(): State { 30 | if (location.hash.length > 0) { 31 | try { 32 | return JSON.parse(decodeURIComponent(location.hash.substring(1))); 33 | } catch (_) { 34 | } 35 | } 36 | return defaultState; 37 | } 38 | 39 | function saveStateToFragment(state: State) { 40 | location.hash = '#' + encodeURIComponent(JSON.stringify(state)); 41 | } 42 | 43 | window.addEventListener('load', () => { 44 | const jsInput = document.getElementById('input'); 45 | const tsOutput = document.getElementById('output'); 46 | const button = document.getElementById('run'); 47 | const autoRunCheckBox = document.getElementById('autorun'); 48 | const stats = document.getElementById('stats'); 49 | const maxIterations = 50 | document.getElementById('maxIterations'); 51 | 52 | const initialState = readStateFromFragment(); 53 | 54 | jsInput.value = 'content' in initialState ? initialState.content! : ''; 55 | jsInput.addEventListener('input', () => { 56 | saveState(); 57 | autoRun(); 58 | }); 59 | 60 | maxIterations.value = String( 61 | 'maxIterations' in initialState ? initialState.maxIterations! : 62 | defaultState.maxIterations); 63 | maxIterations.addEventListener('input', () => { 64 | saveState(); 65 | autoRun(); 66 | }); 67 | 68 | autoRunCheckBox.checked = 69 | 'autoRun' in initialState ? initialState.autoRun! : true; 70 | autoRunCheckBox.addEventListener('change', () => { 71 | saveState(); 72 | autoRun(); 73 | updateRunVisibility(); 74 | }); 75 | 76 | const manager = addUndoSupport(jsInput, autoRun); 77 | addTabIndentSupport(jsInput, (c) => { 78 | manager.content = c; 79 | autoRun(); 80 | }); 81 | 82 | updateRunVisibility(); 83 | autoRun(); 84 | 85 | function saveState() { 86 | saveStateToFragment({ 87 | content: jsInput.value, 88 | maxIterations: getMaxIterations(), 89 | autoRun: autoRunCheckBox.checked 90 | }); 91 | } 92 | function autoRun() { 93 | if (autoRunCheckBox.checked) run(); 94 | } 95 | function updateRunVisibility() { 96 | button.disabled = autoRunCheckBox.checked; 97 | } 98 | function getMaxIterations() { 99 | return Number.parseInt(maxIterations.value); 100 | } 101 | 102 | function run() { 103 | stats.textContent = `Analyzing...`; 104 | const start = new Date().getTime(); 105 | 106 | const options = { 107 | ...defaultOptions, 108 | debugPasses: true, 109 | // differentiateComputedProperties: true, 110 | maxIterations: getMaxIterations() 111 | }; 112 | 113 | const inputFileName = 'file.js'; 114 | const inputs = { 115 | [inputFileName]: jsInput.value, 116 | // 'bar.js': ` 117 | // // export class Bar {} 118 | // // export function takeBar(x) {} 119 | // export default { 120 | // a: a, 121 | // b: function(x) {}, 122 | // c: 1 123 | // } 124 | // function a(x: {readonly y: {z(): void}}): void { 125 | // x.y.z(); 126 | // } 127 | // ` 128 | } 129 | const {files, metadata} = runTyper(inputs, options) 130 | let output = ''; 131 | for (const fileName in files) { 132 | if (fileName != inputFileName) { 133 | output += `${files[fileName]}\n`; 134 | } 135 | } 136 | output += files[inputFileName]; 137 | tsOutput.value = output; 138 | const time = new Date().getTime() - start; 139 | stats.textContent = `Execution time (${metadata.inferencePasses 140 | } passes): ${time} milliseconds`; 141 | } 142 | button.onclick = run; 143 | }); 144 | -------------------------------------------------------------------------------- /src/passes/update_exports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {ReactorCallback} from '../utils/language_service_reactor'; 3 | import * as nodes from '../utils/nodes'; 4 | import * as symbols from '../utils/symbols'; 5 | import {Mutator} from '../utils/mutator'; 6 | 7 | export const updateExports: ReactorCallback = (fileNames, services, addChange, _) => { 8 | const program = services.getProgram(); 9 | const checker = program.getTypeChecker(); 10 | 11 | for (const sourceFile of program.getSourceFiles()) { 12 | if (fileNames.indexOf(sourceFile.fileName) >= 0) { 13 | const mutator = new Mutator(sourceFile.fileName, addChange); 14 | 15 | const exports = new Map(); 16 | let hasDefaultExport = false; 17 | let defaultExportSym: ts.Symbol | undefined; 18 | 19 | ts.forEachChild(sourceFile, visitDefaultExport); 20 | if (exports.size > 0 || defaultExportSym) { 21 | ts.forEachChild(sourceFile, visitExports); 22 | } 23 | 24 | function visitDefaultExport(node: ts.Node) { 25 | if (nodes.isExpressionStatement(node) && nodes.isBinaryExpression(node.expression) && nodes.isEqualsToken(node.expression.operatorToken)) { 26 | const {left, right} = node.expression; 27 | if (nodes.isPropertyAccessExpression(left) && nodes.isIdentifier(left.expression) && 28 | left.name.text == 'exports' && left.expression.text == 'module') { 29 | if (nodes.isObjectLiteralExpression(right)) { 30 | let canExportSeparately = true; 31 | 32 | for (const prop of right.properties) { 33 | // TODO: handle nodes.isShorthandPropertyAssignment(prop) 34 | if (nodes.isPropertyAssignment(prop) && nodes.isIdentifier(prop.name)) { 35 | const sym = checker.getSymbolAtLocation(prop.initializer || prop.name); 36 | 37 | function isRewritable(d: ts.Declaration) { 38 | return d.getSourceFile() == sourceFile && 39 | !isVarWithSiblings(d) && 40 | (!nodes.isVariableDeclaration(d) || nodes.isIdentifier(d.name)); 41 | } 42 | if (sym && sym.getDeclarations().every(isRewritable)) { 43 | exports.set(sym, prop.name.text); 44 | continue; 45 | } 46 | } 47 | canExportSeparately = false; 48 | } 49 | 50 | if (canExportSeparately) { 51 | mutator.removeNode(node); 52 | } else { 53 | mutator.remove({start: node.getStart(), end: right.getStart()}, `export default `); 54 | exports.clear(); 55 | } 56 | } else { 57 | hasDefaultExport = true; 58 | if (nodes.isIdentifier(right)) { 59 | const sym = checker.getSymbolAtLocation(right); 60 | if (sym && !(sym.declarations && sym.declarations.some(isVarWithSiblings))) { 61 | mutator.removeNode(node); 62 | defaultExportSym = sym; 63 | } 64 | } else { 65 | mutator.remove({start: node.getStart(), end: right.getStart()}, `export default `); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | function visitExports(node: ts.Node) { 73 | if (nodes.isClassDeclaration(node) || 74 | nodes.isFunctionDeclaration(node)) { 75 | node.name && handleExport(symbols.getSymbol(node, checker), node, node.name.text); 76 | } else if (nodes.isVariableStatement(node) && node.declarationList.declarations.length == 1) { 77 | const decl = node.declarationList.declarations[0]; 78 | if (nodes.isIdentifier(decl.name)) { 79 | handleExport(symbols.getSymbol(decl, checker), node, decl.name.text); 80 | } 81 | } 82 | } 83 | 84 | function handleExport(sym: ts.Symbol | undefined, node: ts.Node, name: string) { 85 | // const sym = symbols.getSymbol(node, checker); 86 | if (sym) { 87 | if (sym === defaultExportSym) { 88 | mutator.insert(node.getStart(), 'export default '); 89 | } else { 90 | const exportedName = exports.get(sym); 91 | if (exportedName) { 92 | if (name == exportedName) { 93 | // for (const decl of sym.getDeclarations()) { 94 | mutator.insert(node.getStart(), 'export '); 95 | // } 96 | } else { 97 | // for (const decl of sym.getDeclarations()) { 98 | mutator.insert(node.getEnd(), `\nexport {${name} as ${exportedName}};\n`); 99 | // } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | }; 108 | 109 | function isVarWithSiblings(node: ts.Node): boolean { 110 | return nodes.isVariableDeclaration(node) && 111 | nodes.isVariableDeclarationList(node.parent) && 112 | node.parent.declarations.length > 1; 113 | } -------------------------------------------------------------------------------- /test/specs/infers from operators.ts: -------------------------------------------------------------------------------- 1 | // SEMI-AUTOGENERATED FILE, PLEASE ONLY EDIT INPUTS. 2 | // 3 | // REGENERATE OUTPUTS AND METADATA WITH `npm run update-specs`. 4 | 5 | import {TestSpec} from '../../src/testing/test_spec'; 6 | 7 | export default { 8 | files: { 9 | 'input.js': ` 10 | (a, b) => { a in b; }; 11 | (a) => { 'foo' in a; }; 12 | 13 | (a, b) => { a instanceof b; }; 14 | (a, b) => { a = b; }; 15 | (a, b = 1) => { a = b; }; 16 | (a, b) => { a + b; }; 17 | (a, b, c = 1) => { c = a + b; }; 18 | (a, b = 1, c = '') => { c = a + b; }; 19 | 20 | (a, b) => { a += b; }; 21 | (a = 1, b) => { a += b; }; 22 | 23 | (a, b) => { a - b; }; 24 | (a, b) => { a -= b; }; 25 | (a, b) => { a * b; }; 26 | (a, b) => { a *= b; }; 27 | (a, b) => { a ** b; }; 28 | (a, b) => { a **= b; }; 29 | (a, b) => { a / b; }; 30 | (a, b) => { a /= b; }; 31 | (a, b) => { a % b; }; 32 | (a, b) => { a %= b; }; 33 | (a, b) => { a << b; }; 34 | (a, b) => { a <<= b; }; 35 | (a, b) => { a >> b; }; 36 | (a, b) => { a >>= b; }; 37 | (a, b) => { a >>> b; }; 38 | (a, b) => { a >>>= b; }; 39 | (a, b) => { a & b; }; 40 | (a, b) => { a &= b; }; 41 | (a, b) => { a | b; }; 42 | (a, b) => { a |= b; }; 43 | (a, b) => { a ^ b; }; 44 | (a, b) => { a ^= b; }; 45 | 46 | (a, b) => { a == b; }; 47 | (a, b) => { a === b; }; 48 | (a, b) => { a != b; }; 49 | (a, b) => { a !== b; }; 50 | 51 | (a, b) => { a < b; }; 52 | (a, b) => { a <= b; }; 53 | (a, b) => { a > b; }; 54 | (a, b) => { a >= b; }; 55 | 56 | (a = 1, b) => { a < b; }; 57 | (a = 1, b) => { a <= b; }; 58 | (a = 1, b) => { a > b; }; 59 | (a = 1, b) => { a >= b; }; 60 | 61 | (a, b = '') => { a < b; }; 62 | (a, b = '') => { a <= b; }; 63 | (a, b = '') => { a > b; }; 64 | (a, b = '') => { a >= b; }; 65 | 66 | (a, b) => { a || b; }; 67 | (a, b) => { a && b; }; 68 | (a, b) => { a ^^ b; }; 69 | 70 | (a) => { a++; }; 71 | (a) => { ++a; }; 72 | (a) => { a--; }; 73 | (a) => { --a; }; 74 | (a) => { -a; }; 75 | (a) => { +a; }; 76 | (a) => { ~a; }; 77 | (a) => { !a; }; 78 | ` 79 | }, 80 | options: {}, 81 | result: { 82 | files: { 83 | 'input.js': ` 84 | (a: string, b) => { a in b; }; 85 | (a: {readonly foo?: any}) => { 'foo' in a; }; 86 | 87 | (a, b) => { a instanceof b; }; 88 | (a, b) => { a = b; }; 89 | (a: number, b: number = 1) => { a = b; }; 90 | (a, b) => { a + b; }; 91 | (a: number, b: number, c: number = 1) => { c = a + b; }; 92 | (a, b: number = 1, c: string = '') => { c = a + b; }; 93 | 94 | (a, b) => { a += b; }; 95 | (a: number = 1, b: number) => { a += b; }; 96 | 97 | (a: number, b: number) => { a - b; }; 98 | (a: number, b: number) => { a -= b; }; 99 | (a: number, b: number) => { a * b; }; 100 | (a: number, b: number) => { a *= b; }; 101 | (a: number, b: number) => { a ** b; }; 102 | (a: number, b: number) => { a **= b; }; 103 | (a: number, b: number) => { a / b; }; 104 | (a: number, b: number) => { a /= b; }; 105 | (a: number, b: number) => { a % b; }; 106 | (a: number, b: number) => { a %= b; }; 107 | (a: number, b: number) => { a << b; }; 108 | (a: number, b: number) => { a <<= b; }; 109 | (a: number, b: number) => { a >> b; }; 110 | (a: number, b: number) => { a >>= b; }; 111 | (a: number, b: number) => { a >>> b; }; 112 | (a: number, b: number) => { a >>>= b; }; 113 | (a: number, b: number) => { a & b; }; 114 | (a: number, b: number) => { a &= b; }; 115 | (a: number, b: number) => { a | b; }; 116 | (a: number, b: number) => { a |= b; }; 117 | (a: number, b: number) => { a ^ b; }; 118 | (a: number, b: number) => { a ^= b; }; 119 | 120 | (a, b) => { a == b; }; 121 | (a, b) => { a === b; }; 122 | (a, b) => { a != b; }; 123 | (a, b) => { a !== b; }; 124 | 125 | (a, b) => { a < b; }; 126 | (a, b) => { a <= b; }; 127 | (a, b) => { a > b; }; 128 | (a, b) => { a >= b; }; 129 | 130 | (a: number = 1, b: number) => { a < b; }; 131 | (a: number = 1, b: number) => { a <= b; }; 132 | (a: number = 1, b: number) => { a > b; }; 133 | (a: number = 1, b: number) => { a >= b; }; 134 | 135 | (a: string, b: string = '') => { a < b; }; 136 | (a: string, b: string = '') => { a <= b; }; 137 | (a: string, b: string = '') => { a > b; }; 138 | (a: string, b: string = '') => { a >= b; }; 139 | 140 | (a: boolean, b: boolean) => { a || b; }; 141 | (a: boolean, b: boolean) => { a && b; }; 142 | (a: number, b: number) => { a ^^ b; }; 143 | 144 | (a: number) => { a++; }; 145 | (a: number) => { ++a; }; 146 | (a: number) => { a--; }; 147 | (a: number) => { --a; }; 148 | (a: number) => { -a; }; 149 | (a: number) => { +a; }; 150 | (a: number) => { ~a; }; 151 | (a: boolean) => { !a; }; 152 | ` 153 | }, 154 | metadata: { 155 | inferencePasses: 2 156 | } 157 | } 158 | } as TestSpec 159 | -------------------------------------------------------------------------------- /src/utils/apply_constraints.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {AddChangeCallback, AddRequirementCallback} from '../utils/language_service_reactor'; 4 | import * as nodes from '../utils/nodes'; 5 | import {TypeConstraints} from '../utils/type_constraints'; 6 | 7 | // TODO: check if a constraint has seen any new info, then as long as some do, 8 | // do our own loop to avoid writing files. 9 | export function applyConstraints( 10 | allConstraints: Map, 11 | requireConstraints: Map, 12 | _addChange: AddChangeCallback, 13 | addRequirement: AddRequirementCallback) { 14 | function addChange(sourceFile: ts.SourceFile, change: ts.TextChange) { 15 | if (change.span.length > 0) { 16 | const source = sourceFile.getFullText(); 17 | const existing = source.substr(change.span.start, change.span.length); 18 | if (existing == change.newText) { 19 | return; 20 | } 21 | } 22 | _addChange(sourceFile.fileName, change); 23 | } 24 | 25 | for (const [moduleName, constraints] of requireConstraints) { 26 | let decls = `declare module "${moduleName}" {\n`; 27 | 28 | for (const [field, fieldConstraints] of [...constraints.fields, ...constraints.computedFields]) { 29 | // const fieldConstraints = constraints.getFieldConstraints(field); 30 | if (fieldConstraints.isPureFunction) { 31 | const [argList, retType] = fieldConstraints.resolveCallableArgListAndReturnType()!; 32 | if (fieldConstraints.getCallConstraints().constructible) { 33 | decls += ` export class ${field} {\n constructor(${argList});\n }\n`; 34 | } else { 35 | decls += ` export function ${field}(${argList}): ${retType};\n`; 36 | } 37 | } else { 38 | const resolved = fieldConstraints.resolve() || 'any'; 39 | decls += ` export ${fieldConstraints.writable ? 'let' : 'const'} ${field}: ${resolved};\n`; 40 | } 41 | } 42 | 43 | decls += '}\n'; 44 | addRequirement(moduleName, decls); 45 | } 46 | for (const [sym, constraints] of allConstraints) { 47 | // if (!constraints.hasChanges) { 48 | // // console.log(`No changes for symbol 49 | // ${checker.symbolToString(sym)}`); 50 | // continue; 51 | // } 52 | const decls = sym.getDeclarations(); 53 | if (!decls || decls.length == 0) { 54 | continue; 55 | } 56 | let [decl] = decls; 57 | 58 | if (nodes.isParameter(decl) || nodes.isVariableDeclaration(decl)) { 59 | handleVarConstraints(constraints, decl); 60 | } else if (nodes.isFunctionLikeDeclaration(decl)) { 61 | const funDecl = decl; 62 | if (constraints.hasCallConstraints) { 63 | const callConstraints = constraints.getCallConstraints(); 64 | // TODO: if (callConstraints.constructible) 65 | 66 | callConstraints.argTypes.forEach((argConstraints, i) => { 67 | const param = funDecl.parameters[i]; 68 | if (param) { 69 | handleVarConstraints(argConstraints, param); 70 | } 71 | }); 72 | if (!nodes.isArrowFunction(decl)) { 73 | handleReturnType(callConstraints.returnType, funDecl); 74 | } 75 | } 76 | } 77 | 78 | function handleReturnType( 79 | constraints: TypeConstraints, fun: ts.FunctionLikeDeclaration) { 80 | // if (!constraints.hasChanges) { 81 | // return; 82 | // } 83 | const resolved = constraints.resolve(); 84 | if (!resolved) { 85 | return; 86 | } 87 | let start: number; 88 | let length: number; 89 | let pre: string; 90 | if (fun.type) { 91 | start = fun.type.getStart(); 92 | length = fun.type.getEnd() - start; 93 | pre = ''; 94 | } else { 95 | if (fun.body) { 96 | start = fun.body.getStart() - 1; 97 | } else { 98 | start = fun.end; 99 | } 100 | length = 0; 101 | pre = ': '; 102 | } 103 | addChange( 104 | fun.getSourceFile(), 105 | {span: {start: start, length: length}, newText: pre + resolved}); 106 | } 107 | 108 | function handleVarConstraints( 109 | constraints: TypeConstraints, 110 | varDecl: ts.ParameterDeclaration|ts.VariableDeclaration) { 111 | // if (!constraints.hasChanges) { 112 | // return; 113 | // } 114 | // const resolved = constraints.resolve(); 115 | let {isUndefined, resolved} = nodes.isParameter(varDecl) 116 | ? constraints.resolveMaybeUndefined() 117 | : {isUndefined: false, resolved: constraints.resolve()}; 118 | 119 | if (resolved == null) { 120 | if (isUndefined) { 121 | resolved = 'any'; 122 | } else { 123 | return; 124 | } 125 | } 126 | const sourceFile = decl.getSourceFile(); 127 | 128 | const newText = (isUndefined ? '?: ' : ': ') + resolved; 129 | if (nodes.isArrowFunction(varDecl.parent) && 130 | varDecl.parent.parameters.length == 1) { 131 | const arrowStart = varDecl.parent.equalsGreaterThanToken.getStart(); 132 | const space = sourceFile.getFullText().substring(varDecl.getEnd(), arrowStart).trim(); 133 | if (space == '') { 134 | const start = varDecl.getStart(); 135 | const parenthesizedArg = `(${varDecl.getFullText().trim()}${newText}) `; 136 | addChange( 137 | sourceFile, 138 | {span: {start, length: arrowStart - start}, newText: parenthesizedArg}); 139 | return; 140 | } else if (space != ')') { 141 | return; 142 | } 143 | } 144 | 145 | const start = varDecl.name.getEnd(); 146 | const length = varDecl.type ? varDecl.type.getEnd() - start : 0; 147 | addChange( 148 | sourceFile, 149 | {span: {start: start, length: length}, newText}); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/utils/operators.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {TypeConstraints} from './type_constraints'; 3 | import * as flags from './flags'; 4 | import * as nodes from './nodes'; 5 | import {ConstraintsCache} from './constraints_cache'; 6 | 7 | export function inferBinaryOpConstraints(node: ts.BinaryExpression, 8 | resultType: ts.Type, 9 | checker: ts.TypeChecker, 10 | constraintsCache: ConstraintsCache) { 11 | 12 | const leftType = checker.getTypeAtLocation(node.left); 13 | const rightType = checker.getTypeAtLocation(node.right); 14 | 15 | const leftConstraints = constraintsCache.getNodeConstraints(node.left); 16 | const rightConstraints = constraintsCache.getNodeConstraints(node.right); 17 | 18 | const op = node.operatorToken.kind; 19 | 20 | switch (op) { 21 | case ts.SyntaxKind.InKeyword: 22 | leftConstraints && leftConstraints.isString(); 23 | if (nodes.isStringLiteral(node.left)) { 24 | rightConstraints && rightConstraints.getFieldConstraints({name: node.left.text, isNameComputed: true}).isUndefined(); 25 | } 26 | break; 27 | // Plus operators 28 | case ts.SyntaxKind.PlusToken: 29 | case ts.SyntaxKind.PlusEqualsToken: 30 | if (flags.isNumber(resultType) && !flags.isString(resultType)) { 31 | leftConstraints && leftConstraints.isNumber(); 32 | rightConstraints && rightConstraints.isNumber(); 33 | } 34 | break; 35 | // Number operators. 36 | case ts.SyntaxKind.MinusToken: 37 | case ts.SyntaxKind.MinusEqualsToken: 38 | case ts.SyntaxKind.AsteriskToken: 39 | case ts.SyntaxKind.AsteriskEqualsToken: 40 | case ts.SyntaxKind.AsteriskAsteriskToken: 41 | case ts.SyntaxKind.AsteriskAsteriskEqualsToken: 42 | case ts.SyntaxKind.SlashToken: 43 | case ts.SyntaxKind.SlashEqualsToken: 44 | case ts.SyntaxKind.PercentToken: 45 | case ts.SyntaxKind.PercentEqualsToken: 46 | case ts.SyntaxKind.LessThanLessThanToken: 47 | case ts.SyntaxKind.LessThanLessThanEqualsToken: 48 | case ts.SyntaxKind.GreaterThanGreaterThanToken: 49 | case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: 50 | case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: 51 | case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: 52 | case ts.SyntaxKind.AmpersandToken: 53 | case ts.SyntaxKind.AmpersandEqualsToken: 54 | case ts.SyntaxKind.BarToken: 55 | case ts.SyntaxKind.BarEqualsToken: 56 | case ts.SyntaxKind.CaretToken: 57 | case ts.SyntaxKind.CaretEqualsToken: 58 | leftConstraints && leftConstraints.isNumber(); 59 | rightConstraints && rightConstraints.isNumber(); 60 | break; 61 | // Ordering operators 62 | case ts.SyntaxKind.LessThanToken: 63 | case ts.SyntaxKind.LessThanEqualsToken: 64 | case ts.SyntaxKind.GreaterThanToken: 65 | case ts.SyntaxKind.GreaterThanEqualsToken: 66 | { 67 | function handle(constraints: TypeConstraints, otherType: ts.Type) { 68 | if (flags.isNumber(otherType) && !flags.isString(otherType)) { 69 | constraints.isNumber(); 70 | } else if (flags.isString(otherType) && !flags.isNumber(otherType)) { 71 | constraints.isString(); 72 | } 73 | } 74 | leftConstraints && handle(leftConstraints, rightType); 75 | rightConstraints && handle(rightConstraints, leftType); 76 | } 77 | break; 78 | // Boolean operators 79 | case ts.SyntaxKind.AmpersandAmpersandToken: 80 | case ts.SyntaxKind.BarBarToken: 81 | leftConstraints && leftConstraints.isBooleanLike(); 82 | // In `a && b`, we know that `a` is bool-like but know absolutely 83 | // nothing about `b`. 84 | // But if that is embedded in `(a && b) && c`, then it's another 85 | // game. 86 | // TODO: propagate contextual type upwards. 87 | rightConstraints && rightConstraints.isBooleanLike(); 88 | break; 89 | // Equality-like operators 90 | case ts.SyntaxKind.EqualsEqualsToken: 91 | case ts.SyntaxKind.EqualsEqualsEqualsToken: 92 | case ts.SyntaxKind.ExclamationEqualsToken: 93 | case ts.SyntaxKind.ExclamationEqualsEqualsToken: 94 | { 95 | function handle(constraints: TypeConstraints, otherType: ts.Type) { 96 | // if (constraints && otherType) { 97 | if (flags.isPrimitive(otherType)) { //! isAny(otherType)) { 98 | constraints.isType(otherType); 99 | } else if (flags.isNull(otherType)) { 100 | constraints.isNullable(); 101 | } 102 | // } 103 | } 104 | leftConstraints && handle(leftConstraints, rightType); 105 | rightConstraints && handle(rightConstraints, leftType); 106 | } 107 | 108 | if (nodes.isTypeOfExpression(node.left)) { 109 | const constraints = constraintsCache.getNodeConstraints(node.left.expression); 110 | if (constraints && nodes.isStringLiteral(node.right)) { 111 | switch (node.right.text) { 112 | case 'string': 113 | constraints.isString(); 114 | break; 115 | case 'number': 116 | constraints.isNumber(); 117 | break; 118 | case 'boolean': 119 | constraints.isBoolean(); 120 | break; 121 | case 'function': 122 | constraints.getCallConstraints(); 123 | break; 124 | case 'undefined': 125 | constraints.isUndefined(); 126 | break; 127 | case 'symbol': 128 | constraints.isSymbol(); 129 | break; 130 | } 131 | } 132 | } 133 | // leftConstraints && rightType && leftConstraints.isType(rightType); 134 | // rightConstraints && leftType && rightConstraints.isType(leftType); 135 | break; 136 | } 137 | 138 | switch (op) { 139 | // Assignment operators 140 | case ts.SyntaxKind.PlusEqualsToken: 141 | leftConstraints && leftConstraints.isWritable(); 142 | break; 143 | case ts.SyntaxKind.EqualsToken: 144 | case ts.SyntaxKind.MinusEqualsToken: 145 | case ts.SyntaxKind.AsteriskEqualsToken: 146 | case ts.SyntaxKind.AsteriskAsteriskEqualsToken: 147 | case ts.SyntaxKind.SlashEqualsToken: 148 | case ts.SyntaxKind.PercentEqualsToken: 149 | case ts.SyntaxKind.LessThanLessThanEqualsToken: 150 | case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: 151 | case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: 152 | case ts.SyntaxKind.AmpersandEqualsToken: 153 | case ts.SyntaxKind.BarEqualsToken: 154 | case ts.SyntaxKind.CaretEqualsToken: 155 | leftConstraints && leftConstraints.isWritable(); 156 | leftConstraints && !flags.isAny(rightType) && leftConstraints.isType(rightType); 157 | break; 158 | } 159 | 160 | switch (op) { 161 | // Plus operators 162 | // case ts.SyntaxKind.PlusToken: 163 | // rightConstraints && flags.isNumber(leftType) && rightConstraints.isNumber(); 164 | // break; 165 | case ts.SyntaxKind.PlusEqualsToken: 166 | leftConstraints && flags.isNumber(leftConstraints.flags) && rightConstraints && rightConstraints.isNumber(); 167 | // rightConstraints && flags.isNumber(leftType) && rightConstraints.isNumber(); 168 | break; 169 | } 170 | // // handlePlusOp(leftConstraints, rightType); 171 | // } else if (leftConstraints && rightType && !fl.isAny(rightType)) { 172 | // leftConstraints.isType(rightType); 173 | // } 174 | } 175 | 176 | export function inferUnaryOpConstraints( 177 | op: ts.PrefixUnaryOperator | ts.PostfixUnaryOperator, 178 | constraints?: TypeConstraints) { 179 | if (!constraints) { 180 | return; 181 | } 182 | switch (op) { 183 | case ts.SyntaxKind.PlusToken: 184 | case ts.SyntaxKind.PlusPlusToken: 185 | case ts.SyntaxKind.MinusToken: 186 | case ts.SyntaxKind.MinusMinusToken: 187 | case ts.SyntaxKind.TildeToken: 188 | constraints.isNumber(); 189 | break; 190 | case ts.SyntaxKind.ExclamationToken: 191 | constraints.isBooleanLike(); 192 | break; 193 | } 194 | } 195 | 196 | // function set(values: T[]) { 197 | // return values.reduce((s, o) => s.add(o), new Set()); 198 | // } -------------------------------------------------------------------------------- /src/utils/constraints_cache.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Options} from '../options'; 4 | import * as fl from '../utils/flags'; 5 | import * as nodes from '../utils/nodes'; 6 | import {TypeConstraints} from '../utils/type_constraints'; 7 | 8 | export class ConstraintsCache { 9 | allConstraints = new Map(); 10 | requireConstraints = new Map(); 11 | 12 | constructor( 13 | private services: ts.LanguageService, private options: Options, 14 | private checker: ts.TypeChecker) {} 15 | 16 | get hasChanges() { 17 | for (const c of this.allConstraints.values()) { 18 | if (c.hasChanges) return true; 19 | } 20 | return false; 21 | } 22 | nodeIsBooleanLike(node: ts.Node) { 23 | const constraints = this.getNodeConstraints(node); 24 | if (constraints) { 25 | constraints.isBooleanLike(); 26 | } 27 | } 28 | 29 | private getRequireConstraints(requirePath: string): TypeConstraints { 30 | let constraints = this.requireConstraints.get(requirePath); 31 | if (!constraints) { 32 | // console.log(`Building constraints for 33 | // ${this.checker.symbolToString(sym)}`); 34 | constraints = new TypeConstraints( 35 | `require(${requirePath})`, this.services, this.checker, 36 | this.options, undefined); 37 | this.requireConstraints.set(requirePath, constraints); 38 | } 39 | return constraints; 40 | } 41 | 42 | private getSymbolConstraintsAtLocation(node?: ts.Node): TypeConstraints | undefined { 43 | const sym = node && this.checker.getSymbolAtLocation(node); 44 | return sym && this.getSymbolConstraints(sym); 45 | } 46 | 47 | private getSymbolConstraints(sym?: ts.Symbol): TypeConstraints | undefined { 48 | if (!sym) { 49 | return undefined; 50 | } 51 | let constraints = this.allConstraints.get(sym); 52 | if (!constraints) { 53 | // console.log(`Building constraints for 54 | // ${this.checker.symbolToString(sym)}`); 55 | const decls = sym.getDeclarations(); 56 | let type: ts.Type|undefined; 57 | if (decls && decls.length > 0) { 58 | type = this.checker.getTypeOfSymbolAtLocation(sym, decls[0]); 59 | } 60 | constraints = new TypeConstraints( 61 | this.checker.symbolToString(sym), this.services, this.checker, 62 | this.options, type && !fl.isAny(type) ? type : undefined); 63 | this.allConstraints.set(sym, constraints); 64 | } 65 | return constraints; 66 | } 67 | 68 | getNodeConstraints(node: ts.Node): TypeConstraints|undefined { 69 | // console.log(`getNodeConstraints(${nodes.getNodeKindDebugDescription(node)})`); 70 | 71 | while (nodes.isParenthesizedExpression(node)) { 72 | node = node.expression; 73 | } 74 | 75 | const getModuleSymbol = (importClause: ts.ImportClause) => { 76 | if (nodes.isImportDeclaration(importClause.parent)) { 77 | const importDecl = importClause.parent; 78 | return this.checker.getSymbolAtLocation(importDecl.moduleSpecifier); 79 | } 80 | return undefined; 81 | } 82 | 83 | if (nodes.isPropertyAccessExpression(node)) { 84 | const constraints = this.getNodeConstraints(node.expression); 85 | if (constraints) { 86 | return constraints.getFieldConstraints({name: node.name.text, isNameComputed: false}); 87 | } 88 | } else if (nodes.isElementAccessExpression(node) && nodes.isStringLiteral(node.argumentExpression)) { 89 | const constraints = this.getNodeConstraints(node.expression); 90 | if (constraints) { 91 | return constraints.getFieldConstraints({name: node.argumentExpression.text, isNameComputed: true}); 92 | } 93 | } else if (nodes.isCallExpression(node)) { 94 | let constraints = this.getNodeConstraints(node.expression); 95 | if (constraints) { 96 | return constraints.getCallConstraints().returnType; 97 | } 98 | } else if ( 99 | nodes.isConstructor(node) || nodes.isArrowFunction(node) || 100 | nodes.isFunctionExpression(node)) { 101 | // this.checker.getSymbolAtLocation(node); 102 | return this.getSymbolConstraints(node['symbol']); 103 | } else if (nodes.isVariableDeclaration(node)) { 104 | let requiredPath = nodes.getRequiredPath(node.initializer); 105 | if (requiredPath && !requiredPath.startsWith('.')) { 106 | return this.getRequireConstraints(requiredPath); 107 | } 108 | return this.getSymbolConstraintsAtLocation(node.name); 109 | } else if (nodes.isFunctionLikeDeclaration(node) || nodes.isInterfaceDeclaration(node)) { 110 | return this.getSymbolConstraintsAtLocation(node.name); 111 | } else if (nodes.isParameter(node)) { 112 | if (node.parent && nodes.isFunctionLikeDeclaration(node.parent)) { 113 | const paramIndex = node.parent.parameters.indexOf(node); 114 | const funConstraints = this.getNodeConstraints(node.parent); 115 | // console.log(`FUN constraints: ${funConstraints}`); 116 | if (funConstraints) { 117 | return funConstraints.getCallConstraints().getArgType(paramIndex); 118 | } 119 | } 120 | // } else if (nodes.isNamespaceImport(node)) { 121 | // return node.parent && this.getNodeConstraints(node.parent) || 122 | // this.getSymbolConstraintsAtLocation(node); 123 | } else if (nodes.isImportSpecifier(node)) { 124 | if (nodes.isNamedImports(node.parent)) { 125 | const namedImports = node.parent; 126 | if (nodes.isImportClause(namedImports.parent)) { 127 | const modSym = getModuleSymbol(namedImports.parent); 128 | if (modSym && modSym.exports && nodes.isIdentifier(node.name)) { 129 | const sym = modSym.exports[node.name.text]; 130 | return this.getSymbolConstraints(sym); 131 | } 132 | } 133 | } 134 | } else if (nodes.isNamespaceImport(node)) { 135 | if (nodes.isImportClause(node.parent)) { 136 | const modSym = getModuleSymbol(node.parent); 137 | if (modSym && modSym.exports) { 138 | if ('default' in modSym.exports) { 139 | return this.getSymbolConstraints(modSym.exports['default']); 140 | } else { 141 | const constraints = new TypeConstraints(`(module '${this.checker.symbolToString(modSym)}')`, 142 | this.services, this.checker, {...this.options, differentiateComputedProperties: false}); 143 | for (const key in modSym.exports) { 144 | const exp = modSym.exports[key]; 145 | const symConstraints = this.getSymbolConstraints(exp); 146 | if (symConstraints) { 147 | constraints.fields.set(key, symConstraints); 148 | } 149 | } 150 | return constraints; 151 | } 152 | } 153 | } 154 | } else if (nodes.isIdentifier(node)) { 155 | const sym = this.checker.getSymbolAtLocation(node) || node['symbol'] as ts.Symbol; 156 | if (sym) { 157 | const decls = sym.getDeclarations(); 158 | // console.log(`DECLS for idend ${node.getFullText()}: ${decls && 159 | // decls.length}`); 160 | if (decls) { 161 | for (const decl of decls) { 162 | const constraints = this.getNodeConstraints(decl); 163 | if (constraints) { 164 | return constraints; 165 | } 166 | } 167 | } 168 | } 169 | } 170 | 171 | return this.getContextualNodeConstraints(node); 172 | } 173 | 174 | getContextualNodeConstraints(node: ts.Node) { 175 | if (node.parent) { 176 | if (nodes.isReturnStatement(node.parent)) { 177 | if (node === node.parent.expression) { 178 | const enclosingCallable = nodes.findParent(node.parent, nodes.isFunctionLikeDeclaration); 179 | if (enclosingCallable) { 180 | const constraints = this.getNodeConstraints(enclosingCallable); 181 | if (constraints) { 182 | return constraints.getCallConstraints().returnType; 183 | } 184 | } 185 | } 186 | } else if (nodes.isVariableDeclaration(node.parent) || nodes.isPropertyAssignment(node.parent)) { 187 | if (node === node.parent.initializer) { 188 | return this.getNodeConstraints(node.parent); 189 | } 190 | } 191 | } 192 | return undefined; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/passes/infer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Options} from '../options'; 4 | import {applyConstraints} from '../utils/apply_constraints'; 5 | import {ConstraintsCache} from '../utils/constraints_cache'; 6 | import {TypeConstraints} from '../utils/type_constraints'; 7 | import {ReactorCallback} from '../utils/language_service_reactor'; 8 | import {guessName} from '../utils/name_guesser'; 9 | // import * as flags from '../utils/flags'; 10 | import * as nodes from '../utils/nodes'; 11 | import * as ops from '../utils/operators'; 12 | import * as objects from '../matchers/objects'; 13 | import {CallConstraints} from '../utils/type_constraints'; 14 | 15 | export const infer: (options: Options) => ReactorCallback = (options) => ( 16 | fileNames, services, addChange, addRequirement) => { 17 | 18 | const program = services.getProgram(); 19 | const checker = program.getTypeChecker(); 20 | const constraintsCache = new ConstraintsCache(services, options, checker); 21 | 22 | function inferOnce() { 23 | for (const sourceFile of program.getSourceFiles()) { 24 | if (fileNames.indexOf(sourceFile.fileName) < 0) { 25 | console.warn(`SKIPPING ${sourceFile.fileName}`); 26 | } 27 | if (fileNames.indexOf(sourceFile.fileName) >= 0) { 28 | nodes.traverse(sourceFile, (node: ts.Node) => { 29 | const nodeConstraints = constraintsCache.getNodeConstraints(node); 30 | const ctxType = checker.getContextualType( 31 | node); // TODO: isExpression 32 | if (nodeConstraints) { 33 | // Don't propagate contextual type up of `test ? x : null`, as x 34 | // will be inferred to be nullable. 35 | if (node.parent && !nodes.isConditionalExpression(node.parent)) { 36 | nodeConstraints.isType(ctxType, { 37 | // isReadonly: false, 38 | andNotFlags: ts.TypeFlags.Undefined | ts.TypeFlags.Null 39 | }); 40 | 41 | if (!nodes.isExpressionStatement(node.parent)) { 42 | nodeConstraints.cannotBeVoid(); 43 | } 44 | } 45 | } 46 | 47 | function defineProperty(objectConstraints: TypeConstraints | undefined, name: string, isNameComputed: boolean, desc?: objects.MatchedPropertyDescriptor) { 48 | if (!desc || !objectConstraints) return; 49 | 50 | const fieldConstraints = 51 | objectConstraints.getFieldConstraints({name, isNameComputed}); 52 | 53 | for (const propType of desc.valueTypes) { 54 | fieldConstraints.isType(propType); 55 | fieldConstraints.isUndefined(); 56 | } 57 | if (desc.writable) { 58 | fieldConstraints.isWritable(); 59 | } 60 | } 61 | 62 | if (nodes.isCallExpression(node)) { 63 | const constraints = constraintsCache.getNodeConstraints(node.expression); 64 | if (constraints) { 65 | fillCallConstraints( 66 | constraints.getCallConstraints(), 67 | ctxType, 68 | node.parent != null && nodes.isExpressionStatement(node.parent), 69 | node.arguments); 70 | } 71 | const simpleSelect = objects.matchSimpleSelect(node.expression); 72 | if (simpleSelect && simpleSelect.targetName == 'Object') { 73 | switch (simpleSelect.selectedName) { 74 | case 'defineProperty': 75 | if (node.arguments.length == 3) { 76 | const [target, name, desc] = node.arguments; 77 | const objectConstraints = constraintsCache.getNodeConstraints(target); 78 | if (!objectConstraints && !nodeConstraints) break; 79 | 80 | if (nodes.isStringLiteral(name) && nodes.isObjectLiteralExpression(desc)) { 81 | const matchedDec = objects.matchPropertyDescriptor(desc, checker); 82 | defineProperty(objectConstraints, name.text, true, matchedDec); 83 | defineProperty(nodeConstraints, name.text, true, matchedDec); 84 | } 85 | } 86 | break; 87 | case 'defineProperties': 88 | if (node.arguments.length == 2) { 89 | const [target, descs] = node.arguments; 90 | const objectConstraints = constraintsCache.getNodeConstraints(target); 91 | if (!objectConstraints && !nodeConstraints) break; 92 | 93 | if (nodes.isObjectLiteralExpression(descs)) { 94 | const props = objects.matchPropertyDescriptors(descs, checker); 95 | if (props) { 96 | for (const prop of props.properties) { 97 | defineProperty(objectConstraints, prop.name, prop.isNameComputed, prop); 98 | defineProperty(nodeConstraints, prop.name, prop.isNameComputed, prop); 99 | } 100 | } 101 | } 102 | } 103 | break; 104 | case 'create': 105 | if (node.arguments.length == 1 || 106 | node.arguments.length == 2) { 107 | const [protoType, sourceType] = node.arguments.map(n => checker.getTypeAtLocation(n)); 108 | if (nodeConstraints) { 109 | nodeConstraints.isType(protoType); 110 | if (sourceType) nodeConstraints.isType(sourceType); 111 | } 112 | break; 113 | } 114 | break; 115 | case 'assign': 116 | if (node.arguments.length >= 2) { 117 | const [destination, ...sources] = node.arguments; 118 | const destinationType = checker.getTypeAtLocation(destination); 119 | const destinationTypeProps = destinationType.getProperties(); 120 | 121 | function hasExistingProp(matchedName: objects.MatchedDeclarationName) { 122 | if (!destinationTypeProps) return false; 123 | for (const prop of destinationTypeProps) { 124 | const decls = prop.declarations; 125 | if (decls) { 126 | for (const decl of decls) { 127 | if (decl.name) { 128 | const name = objects.matchDeclarationName(decl.name); 129 | if (name && name.name == matchedName.name && 130 | (!options.differentiateComputedProperties || name.isNameComputed == matchedName.isNameComputed)) { 131 | return true; 132 | } 133 | } 134 | } 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | function updateAssignedConstraints(constraints?: TypeConstraints) { 141 | if (!constraints) return; 142 | 143 | for (const source of sources) { 144 | const sourceType = checker.getTypeAtLocation(source); 145 | constraints.isType(sourceType); 146 | for (const prop of sourceType.getProperties()) { 147 | if (prop.declarations) { 148 | for (const decl of prop.declarations) { 149 | if (decl.name) { 150 | const matchedName = objects.matchDeclarationName(decl.name); 151 | if (matchedName) { 152 | const fieldConstraints = constraints.getFieldConstraints(matchedName); 153 | if (//!flags.isAny(destinationType) && 154 | //!flags.isAny(destinationConstraints.flags) && 155 | !hasExistingProp(matchedName)) { 156 | fieldConstraints.isUndefined(); 157 | } 158 | fieldConstraints.isWritable(); 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | updateAssignedConstraints(constraintsCache.getNodeConstraints(destination)); 168 | updateAssignedConstraints(nodeConstraints); 169 | } 170 | break; 171 | // case 'create': 172 | // break; 173 | } 174 | } 175 | } else if (nodes.isNewExpression(node)) { 176 | const constraints = constraintsCache.getNodeConstraints(node.expression); 177 | if (constraints) { 178 | fillCallConstraints( 179 | constraints.getCallConstraints(), 180 | ctxType, 181 | node.parent != null && nodes.isExpressionStatement(node.parent), 182 | node.arguments).isConstructible(); 183 | } 184 | } else if (nodes.isBinaryExpression(node)) { 185 | ops.inferBinaryOpConstraints(node, ctxType, checker, constraintsCache); 186 | } else if (nodes.isUnaryExpression(node)) { 187 | ops.inferUnaryOpConstraints(node.operator, constraintsCache.getNodeConstraints(node.operand)); 188 | } else if (nodes.isDeleteExpression(node)) { 189 | const constraints = constraintsCache.getNodeConstraints(node.expression); 190 | if (constraints) { 191 | constraints.isWritable(); 192 | constraints.isUndefined(); 193 | } 194 | } else if (nodes.isIfStatement(node)) { 195 | constraintsCache.nodeIsBooleanLike(node.expression); 196 | } else if (nodes.isConditionalExpression(node)) { 197 | constraintsCache.nodeIsBooleanLike(node.condition); 198 | } 199 | }); 200 | } 201 | } 202 | } 203 | 204 | function fillCallConstraints(callConstraints: CallConstraints, returnType: ts.Type | undefined, isVoid: boolean, args?: ts.Node[]) { 205 | if (!args) { 206 | args = []; 207 | } 208 | const argTypes = args.map(a => checker.getTypeAtLocation(a)) || []; 209 | 210 | if (returnType) { 211 | callConstraints.returnType.isType(returnType); 212 | } else if (isVoid) { 213 | callConstraints.returnType.isVoid(); 214 | } 215 | 216 | // console.log(`CALL(${call.getFullText()}):`); 217 | callConstraints.hasArity(argTypes.length); 218 | argTypes.forEach((t, i) => { 219 | const argConstraints = callConstraints.getArgType(i); 220 | const arg = args![i]; 221 | if (arg) { 222 | argConstraints.addNameHint(guessName(arg)); 223 | } 224 | // console.log(` ARG(${i}): ${checker.typeToString(t)}`); 225 | argConstraints.isType(t, {isReadonly: true}); 226 | }); 227 | 228 | return callConstraints; 229 | } 230 | 231 | 232 | // TODO: check if a constraint has seen any new info, then as long as some do, 233 | // do our own loop to avoid writing files. 234 | // for (let i = 0; i < options.maxSubInferenceCount; i++) { 235 | inferOnce(); 236 | // if (!constraintsCache.hasChanges) { 237 | // break; 238 | // } 239 | // } 240 | 241 | applyConstraints(constraintsCache.allConstraints, constraintsCache.requireConstraints, addChange, addRequirement); 242 | } 243 | -------------------------------------------------------------------------------- /src/utils/type_constraints.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import {Options} from '../options'; 4 | import * as nodes from './nodes'; 5 | import * as objects from '../matchers/objects'; 6 | import * as fl from './flags'; 7 | import {CallConstraints} from './call_constraints'; 8 | export {CallConstraints}; 9 | 10 | export type TypeOptions = { 11 | markChanges: boolean, 12 | isReadonly: boolean, 13 | andNotFlags: ts.TypeFlags 14 | }; 15 | 16 | export type TypeOpts = Partial>; 17 | 18 | export const defaultTypeOptions: Readonly = { 19 | markChanges: true, 20 | isReadonly: false, 21 | andNotFlags: 0 22 | }; 23 | 24 | export function getTypeOptions(opts?: TypeOpts, defaults = defaultTypeOptions): Readonly { 25 | return opts ? {...defaults, ...opts} : defaultTypeOptions; 26 | } 27 | 28 | export class TypeConstraints { 29 | fields = new Map(); 30 | computedFields = new Map(); 31 | 32 | // private constructConstraints?: CallConstraints; 33 | private _callConstraints?: CallConstraints; 34 | private symbols: ts.Symbol[] = []; 35 | private names: Set|undefined; 36 | private nameHints: Set|undefined; 37 | private _flags: ts.TypeFlags = 0; 38 | private _isBooleanLike = false; 39 | private _isSymbol = false; 40 | private _isNumberOrString = false; 41 | private _cannotBeVoid = false; 42 | private _isWritable = false; 43 | 44 | private _hasChanges = false; 45 | 46 | constructor( 47 | public readonly description: string, private services: ts.LanguageService, 48 | private checker: ts.TypeChecker, private options: Options, 49 | initialType?: ts.Type) { 50 | if (initialType) { 51 | this.isType(initialType); 52 | } 53 | } 54 | 55 | addName(name?: string) { 56 | if (!name) return; 57 | (this.names = this.names || new Set()).add(name); 58 | } 59 | addNameHint(name?: string) { 60 | if (!name) return; 61 | (this.nameHints = this.nameHints || new Set()).add(name); 62 | } 63 | 64 | get flags() { 65 | return this._flags; 66 | } 67 | 68 | private createConstraints(description: string, initialType?: ts.Type): 69 | TypeConstraints { 70 | return new TypeConstraints( 71 | description, this.services, this.checker, this.options, initialType); 72 | } 73 | 74 | get isPureFunction(): boolean { 75 | return this._callConstraints != null && 76 | //!this._callConstraints.constructible && 77 | !this._isBooleanLike && 78 | !this._isSymbol && 79 | !this._isNumberOrString && (this._flags === ts.TypeFlags.Object) && 80 | // this.constructConstraints == null && 81 | // !this._isWritable && 82 | this.fields.size == 0 && 83 | this.computedFields.size == 0; 84 | } 85 | 86 | getFieldConstraints({name, isNameComputed}: {name: string, isNameComputed: boolean}, opts?: TypeOpts): 87 | TypeConstraints { 88 | 89 | const computed = isNameComputed && this.options.differentiateComputedProperties; 90 | const map = computed ? this.computedFields : this.fields; 91 | 92 | this.isObject(opts); 93 | let constraints = map.get(name); 94 | if (!constraints) { 95 | constraints = this.createConstraints(`${this.description}${computed ? `['${name}']` : `.${name}`}`); 96 | constraints.markChange(opts); 97 | map.set(name, constraints); 98 | } 99 | return constraints; 100 | } 101 | 102 | isType(type: ts.Type, opts?: TypeOpts) { 103 | let typeOptions = getTypeOptions(opts); 104 | 105 | if (!type || fl.isAny(type)) { 106 | if (!this._flags && this.symbols.length == 0) { 107 | this.markChange(typeOptions); 108 | } 109 | return; 110 | } 111 | 112 | const flags = type.flags & ~typeOptions.andNotFlags; 113 | 114 | if (flags & ts.TypeFlags.Object && type.symbol && 115 | type.symbol.flags & 116 | (ts.SymbolFlags.Class | ts.SymbolFlags.Enum | 117 | ts.SymbolFlags.ConstEnum | ts.SymbolFlags.Interface | 118 | ts.SymbolFlags.Alias | ts.SymbolFlags.TypeAlias | 119 | ts.SymbolFlags.TypeParameter)) { 120 | // console.log(`GOT SYMBOL ${this.checker.symbolToString(ft.symbol)}`); 121 | if (this.symbols.indexOf(type.symbol) >= 0) { 122 | return; 123 | } 124 | this.symbols.push(type.symbol); 125 | this.markChange(typeOptions); 126 | typeOptions = {...typeOptions, markChanges: false}; 127 | } 128 | 129 | // const originalHasChanges = this._hasChanges; 130 | 131 | if (fl.isUnion(type)) { 132 | for (const member of (type).types) { 133 | this.isType(member, typeOptions); 134 | } 135 | if (flags === ts.TypeFlags.Union) { 136 | // Nothing else in this union type. 137 | return; 138 | } 139 | } 140 | 141 | for (const sig of type.getCallSignatures()) { 142 | const cc = this.getCallConstraints(typeOptions); 143 | cc.hasSignature(sig, typeOptions); 144 | } 145 | 146 | for (const sig of type.getConstructSignatures()) { 147 | const cc = this.getCallConstraints(typeOptions); 148 | cc.hasSignature(sig, typeOptions); 149 | cc.isConstructible(typeOptions); 150 | } 151 | 152 | for (const prop of type.getProperties()) { 153 | const decls = prop.getDeclarations(); 154 | if (decls == null || decls.length == 0) { 155 | // Declaration is outside the scope of the compilation (maybe, builtin 156 | // JS types). 157 | // console.warn(`Property ${this.checker.symbolToString(prop)} has no 158 | // known declaration`); 159 | continue; 160 | } 161 | // const fieldConstraints = this.getFieldConstraints(this.checker.symbolToString(prop), markChanges); 162 | 163 | for (const decl of decls) { 164 | let matchedName = decl.name && objects.matchDeclarationName(decl.name); 165 | if (!matchedName) { 166 | continue; 167 | } 168 | 169 | let fieldConstraints = this.getFieldConstraints(matchedName, typeOptions); 170 | 171 | const type = this.checker.getTypeOfSymbolAtLocation(prop, decl); 172 | if (!typeOptions.isReadonly &&//(prop.flags & ts.SymbolFlags.SetAccessor) || 173 | !nodes.isReadonly(decl)) { 174 | fieldConstraints.isWritable(typeOptions); 175 | } 176 | if (nodes.isPropertySignature(decl) && decl.questionToken) { 177 | fieldConstraints.isUndefined(typeOptions); 178 | } 179 | fieldConstraints.isType(type, typeOptions); 180 | } 181 | } 182 | 183 | this.hasFlags(flags, typeOptions); 184 | 185 | // if (!markChanges) { 186 | // this._hasChanges = originalHasChanges; 187 | // } 188 | // const after = this.resolve(); 189 | // console.log(`isType(${this.checker.typeToString(type)}): "${before}" -> 190 | // "${after}`); 191 | } 192 | 193 | private hasFlags(flags: ts.TypeFlags, opts?: TypeOpts) { 194 | flags = fl.normalize(flags); 195 | 196 | const oldFlags = this._flags; 197 | const newFlags = oldFlags | flags; 198 | if (newFlags != this._flags) { 199 | this._flags = newFlags; 200 | this.markChange(opts); 201 | } 202 | } 203 | 204 | get hasCallConstraints(): boolean { 205 | return !!this._callConstraints; 206 | } 207 | 208 | getCallConstraints(opts?: TypeOpts): CallConstraints { 209 | this.isObject(opts); 210 | if (!this._callConstraints) { 211 | this._callConstraints = new CallConstraints( 212 | this.checker, `${this.description}.call`, 213 | (d) => this.createConstraints(d));//`${this.description}.call.${d}`)); 214 | } 215 | return this._callConstraints; 216 | } 217 | 218 | isNumber(opts?: TypeOpts) { 219 | this.hasFlags(ts.TypeFlags.Number, opts); 220 | } 221 | isBoolean(opts?: TypeOpts) { 222 | this.hasFlags(ts.TypeFlags.Boolean, opts); 223 | } 224 | isString(opts?: TypeOpts) { 225 | this.hasFlags(ts.TypeFlags.String, opts); 226 | } 227 | isUndefined(opts?: TypeOpts) { 228 | this.hasFlags(ts.TypeFlags.Undefined, opts); 229 | } 230 | isObject(opts?: TypeOpts) { 231 | this.hasFlags(ts.TypeFlags.Object, opts); 232 | } 233 | isNullable(opts?: TypeOpts) { 234 | this.hasFlags(ts.TypeFlags.Null, opts); 235 | } 236 | isVoid(opts?: TypeOpts) { 237 | this.hasFlags(ts.TypeFlags.Void, opts); 238 | } 239 | 240 | get writable(): boolean { 241 | return this._isWritable; 242 | } 243 | isWritable(opts?: TypeOpts) { 244 | if (this._isWritable) return; 245 | this._isWritable = true; 246 | this.markChange(opts); 247 | } 248 | cannotBeVoid(opts?: TypeOpts) { 249 | if (this._cannotBeVoid || this._flags & ~ts.TypeFlags.Void) return; 250 | 251 | this._cannotBeVoid = true; 252 | this.markChange(opts); 253 | } 254 | isSymbol(opts?: TypeOpts) { 255 | if (this._isSymbol) return; 256 | 257 | this._isSymbol = true; 258 | this.markChange(opts); 259 | } 260 | isBooleanLike(opts?: TypeOpts) { 261 | if (this._isBooleanLike || fl.isBoolean(this._flags) || 262 | fl.isBooleanLike(this._flags) || fl.isNullOrUndefined(this._flags) || 263 | fl.isNumberOrString(this._flags)) { 264 | return; 265 | } 266 | this._isBooleanLike = true; 267 | this.markChange(opts); 268 | } 269 | isNumberOrString(opts?: TypeOpts) { 270 | if (this._isNumberOrString || fl.isNumberOrString(this._flags)) { 271 | return; 272 | } 273 | this._isNumberOrString = true; 274 | this.markChange(opts); 275 | // this.hasFlags(ts.TypeFlags.StringOrNumberLiteral); 276 | } 277 | 278 | private markChange(opts?: TypeOpts) { 279 | const options = getTypeOptions(opts); 280 | if (!options.markChanges) return; 281 | 282 | if (this._hasChanges) return; 283 | 284 | // const typeNames = this.types.map(t => 285 | // this.checker.typeToString(t)).join(', '); 286 | this._hasChanges = true; 287 | } 288 | 289 | get hasChanges() { 290 | if (this._hasChanges) { 291 | return true; 292 | } 293 | if ([...this.fields.values(), ...this.computedFields.values()].some(f => f.hasChanges)) { 294 | this._hasChanges = true; 295 | return true; 296 | } 297 | if (this._callConstraints && this._callConstraints.hasChanges) { 298 | this._hasChanges = true; 299 | return true; 300 | } 301 | return false; 302 | } 303 | 304 | resolveMaybeUndefined(): {resolved: string | null, isUndefined: boolean} { 305 | this.normalize(); 306 | const isUndefined = fl.isUndefined(this.flags); 307 | 308 | const resolved = 309 | this.resolve({ignoreFlags: isUndefined ? ts.TypeFlags.Undefined : 0}); 310 | return {resolved: resolved, isUndefined: isUndefined}; 311 | } 312 | 313 | private resolveKeyValueDecl(name: string, valueType: TypeConstraints, {isComputed, isMember}: {isComputed?: boolean, isMember?: boolean} = {}): 314 | string { 315 | valueType.normalize(); 316 | let {isUndefined, resolved} = valueType.resolveMaybeUndefined(); 317 | resolved = resolved || 'any'; 318 | 319 | const key = isComputed ? `['${name}']` : name; 320 | const prefix = valueType._isWritable || !isMember ? '' : 'readonly '; 321 | if (isUndefined) { 322 | return `${prefix}${key}?: ${resolved}`; 323 | } else { 324 | return `${prefix}${key}: ${resolved}`; 325 | // return `get ${key}(): ${resolved}`; 326 | } 327 | } 328 | 329 | get bestName(): string|undefined { 330 | if (this.names) { 331 | for (const name of this.names) { 332 | return name; 333 | } 334 | } 335 | if (this.nameHints) { 336 | const hints = [...this.nameHints.values()]; 337 | const camelSplit = 338 | hints.map(n => n.replace(/([a-z](?=[A-Z]))/g, '$1 ').toLowerCase()); 339 | // TODO: tokenize camel-case instead of this crude test. 340 | return hints.find((_, i) => { 341 | const cs = camelSplit[i]; 342 | return camelSplit.every((cs2, ii) => i == ii || cs2.endsWith(cs)); 343 | }); 344 | } 345 | return undefined; 346 | } 347 | 348 | resolveCallableArgListAndReturnType(): [string, string]|undefined { 349 | const callConstraints = this._callConstraints; 350 | if (callConstraints) { 351 | let args = callConstraints.argTypes.map((t, i) => { 352 | const name = t.bestName || `arg${i + 1}`; 353 | return this.resolveKeyValueDecl(name, t, {isMember: false}); 354 | }); 355 | if (callConstraints.hasThisType()) { 356 | args = [this.resolveKeyValueDecl('this', callConstraints.getThisType()), ...args]; 357 | } 358 | return [ 359 | args.join(', '), 360 | callConstraints.returnType.resolve() || 'any' 361 | ]; 362 | } 363 | return undefined; 364 | } 365 | 366 | normalize() { 367 | let flags = this._flags; 368 | 369 | // let fieldsToRemove = new Set([...this.fields.keys()]); 370 | const removeFields = (names: string[]) => { 371 | for (const n of names) { 372 | this.fields.delete(n); 373 | this.computedFields.delete(n); 374 | } 375 | }; 376 | const removeAllFields = () => { 377 | this.fields.clear(); 378 | this.computedFields.clear(); 379 | }; 380 | 381 | if (fl.isString(flags)) { 382 | removeFields(Object.keys(String.prototype)); 383 | } 384 | 385 | const fieldNames = [...this.fields.keys(), ...this.computedFields.keys()]; 386 | const allFieldsAreStringMembers = 387 | fieldNames.every(k => k in String.prototype); 388 | const allFieldsAreArrayMembers = 389 | fieldNames.every(k => k in Array.prototype); 390 | 391 | // TODO: avoid overwriting literal booleans (which are erased during flag 392 | // normalization) 393 | if (fl.isBoolean(flags)) { 394 | if (fl.isNullOrUndefined(flags)) { 395 | flags &= ~(ts.TypeFlags.Null | ts.TypeFlags.Undefined); 396 | } 397 | // Make boolean yield to string and number, as they both fulfil the same 398 | // bool-like purpose. 399 | if (fl.isString(flags) || fl.isNumber(flags)) { 400 | flags &= ~ts.TypeFlags.Boolean; 401 | } 402 | } 403 | 404 | if (allFieldsAreStringMembers && 405 | (!allFieldsAreArrayMembers || this._isBooleanLike || 406 | fl.isBoolean(flags) || fl.isBooleanLike(flags)) && 407 | fieldNames.length >= 408 | this.options.methodThresholdAfterWhichAssumeString) { 409 | this._isNumberOrString = false; 410 | flags &= ~ts.TypeFlags.Boolean; 411 | flags |= ts.TypeFlags.String; 412 | removeAllFields(); 413 | } 414 | 415 | if (this._isNumberOrString || fl.isNumber(flags) || fl.isString(flags)) { 416 | // if (this._isNumberOrString) { 417 | this._isNumberOrString = false; 418 | 419 | if (fieldNames.length > 0 && allFieldsAreStringMembers) { 420 | flags |= ts.TypeFlags.String; 421 | removeAllFields(); 422 | } else if (!fl.isNumber(flags) && !fl.isString(flags)) { 423 | flags |= ts.TypeFlags.String | ts.TypeFlags.Number; 424 | } 425 | } 426 | if (this._isBooleanLike) { 427 | this._isBooleanLike = false; 428 | if (!fl.isNumber(flags) && !fl.isBoolean(flags) && !fl.isString(flags) && 429 | !fl.isNullOrUndefined(flags)) { 430 | if (fl.isObject(flags) || fl.isStructuredType(flags)) { 431 | flags |= ts.TypeFlags.Undefined; 432 | } else { 433 | flags |= ts.TypeFlags.Boolean; 434 | } 435 | } 436 | } 437 | if (this._cannotBeVoid) { 438 | this._cannotBeVoid = false; 439 | flags &= ~ts.TypeFlags.Void; 440 | } 441 | this._flags = flags; 442 | } 443 | 444 | resolve({ignoreFlags}: {ignoreFlags?: ts.TypeFlags} = {}): string|null { 445 | this.normalize(); 446 | 447 | if (fl.isAny(this.flags)) { 448 | return 'any'; 449 | } 450 | 451 | const union: string[] = []; 452 | const members: string[] = []; 453 | 454 | if (this._callConstraints) { 455 | let [argList, retType] = this.resolveCallableArgListAndReturnType()!; 456 | retType = retType || 'any'; 457 | if (retType.indexOf(' ') >= 0) { 458 | retType = `(${retType})`; 459 | } 460 | 461 | // const sigSig = `(${argList})${retType}`; 462 | const prefix = this._callConstraints.constructible ? 'new' : ''; 463 | if (this.fields.size > 0 || this.computedFields.size > 0) { 464 | members.push(`${prefix}(${argList}): ${retType}`) 465 | } else { 466 | union.push(`${prefix}(${argList}) => ${retType}`); 467 | } 468 | } 469 | 470 | for (const sym of this.symbols) { 471 | union.push(this.checker.symbolToString(sym)); 472 | } 473 | 474 | const handleFields = (fields: Map, isComputed: boolean) => { 475 | for (const [name, constraints] of [...fields].sort(([a], [b]) => a < b ? -1 : 1)) { 476 | if (!constraints.hasChanges) { 477 | continue; 478 | } 479 | constraints.normalize(); 480 | 481 | if (constraints.isPureFunction) { 482 | const [argList, retType] = 483 | constraints!.resolveCallableArgListAndReturnType()!; 484 | members.push(`${isComputed ? `['${name}']` : name}(${argList}): ${retType}`); 485 | } else { 486 | members.push(this.resolveKeyValueDecl(name, constraints, {isComputed, isMember: true})); 487 | } 488 | } 489 | }; 490 | handleFields(this.fields, false); 491 | handleFields(this.computedFields, true); 492 | 493 | let flags = this._flags; 494 | if (ignoreFlags) { 495 | flags &= ~ignoreFlags; 496 | } 497 | for (const primitive in primitivePredicates) { 498 | if (primitivePredicates[primitive](flags)) { 499 | union.push(primitive); 500 | } 501 | } 502 | if (this._isSymbol) { 503 | union.push('symbol'); 504 | } 505 | if (members.length > 0) { 506 | union.push('{' + members.join(', ') + '}'); 507 | } 508 | 509 | // Skip void if there's any other type. 510 | if (union.length == 0 && fl.isVoid(flags)) { 511 | union.push('void'); 512 | } 513 | const result = union.length == 0 ? null : union.map(u => u.indexOf(' ') < 0 || union.length == 1 ? u : `(${u})`).join(' | '); 514 | // console.log(`result = "${result}" (members = [${members}], types = 515 | // [${this.types.map(t => this.checker.typeToString(t))}], flags = 516 | // ${this._flags}, missingFlags = ${missingFlags}`); 517 | return result; 518 | } 519 | } 520 | 521 | const primitivePredicates = { 522 | 'number': fl.isNumber, 523 | 'string': fl.isString, 524 | 'boolean': fl.isBoolean, 525 | 'null': fl.isNull, 526 | 'undefined': fl.isUndefined, 527 | }; -------------------------------------------------------------------------------- /src/utils/node_predicates.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function isUnaryExpression(node: ts.Node): node is (ts.PrefixUnaryExpression | ts.PostfixUnaryExpression) { 4 | return isPostfixUnaryExpression(node) || isPrefixUnaryExpression(node); 5 | } 6 | 7 | export function isFunctionLikeDeclaration(decl?: ts.Node): 8 | decl is ts.FunctionLikeDeclaration { 9 | if (!decl) { 10 | return false; 11 | } 12 | switch (decl.kind) { 13 | case ts.SyntaxKind.ArrowFunction: 14 | case ts.SyntaxKind.FunctionExpression: 15 | 16 | case ts.SyntaxKind.FunctionDeclaration: 17 | case ts.SyntaxKind.MethodDeclaration: 18 | case ts.SyntaxKind.Constructor: 19 | case ts.SyntaxKind.GetAccessor: 20 | case ts.SyntaxKind.SetAccessor: 21 | return true; 22 | default: 23 | return false; 24 | } 25 | } 26 | 27 | export function getParamIndex(param: ts.ParameterDeclaration, fun: ts.FunctionLikeDeclaration): number { 28 | let offset = 0; 29 | while (offset < fun.parameters.length && isThisParam(fun.parameters[offset])) { 30 | offset++; 31 | } 32 | return fun.parameters.indexOf(param, offset); 33 | } 34 | 35 | export function getNthParam(fun: ts.FunctionLikeDeclaration, index: number): ts.ParameterDeclaration { 36 | return fun.parameters.filter(p => !isThisParam(p))[index]; 37 | } 38 | 39 | export function isThisParam(param: ts.ParameterDeclaration): boolean { 40 | return isIdentifier(param.name) && param.name.text == 'this'; 41 | } 42 | 43 | export function isEndOfFileToken(node?: ts.Node): 44 | node is ts.Token { 45 | return node != null && node.kind === ts.SyntaxKind.EndOfFileToken; 46 | } 47 | export function isNumericLiteral(node?: ts.Node): node is ts.NumericLiteral { 48 | return node != null && node.kind === ts.SyntaxKind.NumericLiteral; 49 | } 50 | export function isStringLiteral(node?: ts.Node): node is ts.StringLiteral { 51 | return node != null && node.kind === ts.SyntaxKind.StringLiteral; 52 | } 53 | export function isTrueKeyword(node?: ts.Node): node is ts.Token { 54 | return node != null && node.kind === ts.SyntaxKind.TrueKeyword; 55 | } 56 | export function isFalseKeyword(node?: ts.Node): node is ts.Token { 57 | return node != null && node.kind === ts.SyntaxKind.FalseKeyword; 58 | } 59 | export function isJsxText(node?: ts.Node): node is ts.JsxText { 60 | return node != null && node.kind === ts.SyntaxKind.JsxText; 61 | } 62 | export function isRegularExpressionLiteral(node?: ts.Node): 63 | node is ts.RegularExpressionLiteral { 64 | return node != null && node.kind === ts.SyntaxKind.RegularExpressionLiteral; 65 | } 66 | export function isNoSubstitutionTemplateLiteral(node?: ts.Node): 67 | node is ts.NoSubstitutionTemplateLiteral { 68 | return node != null && node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral; 69 | } 70 | export function isTemplateHead(node?: ts.Node): node is ts.TemplateHead { 71 | return node != null && node.kind === ts.SyntaxKind.TemplateHead; 72 | } 73 | export function isTemplateMiddle(node?: ts.Node): node is ts.TemplateMiddle { 74 | return node != null && node.kind === ts.SyntaxKind.TemplateMiddle; 75 | } 76 | export function isTemplateTail(node?: ts.Node): node is ts.TemplateTail { 77 | return node != null && node.kind === ts.SyntaxKind.TemplateTail; 78 | } 79 | export function isOpenBraceToken(node?: ts.Node): 80 | node is ts.Token { 81 | return node != null && node.kind === ts.SyntaxKind.OpenBraceToken; 82 | } 83 | export function isCloseBraceToken(node?: ts.Node): 84 | node is ts.Token { 85 | return node != null && node.kind === ts.SyntaxKind.CloseBraceToken; 86 | } 87 | export function isOpenParenToken(node?: ts.Node): 88 | node is ts.Token { 89 | return node != null && node.kind === ts.SyntaxKind.OpenParenToken; 90 | } 91 | export function isCloseParenToken(node?: ts.Node): 92 | node is ts.Token { 93 | return node != null && node.kind === ts.SyntaxKind.CloseParenToken; 94 | } 95 | export function isOpenBracketToken(node?: ts.Node): 96 | node is ts.Token { 97 | return node != null && node.kind === ts.SyntaxKind.OpenBracketToken; 98 | } 99 | export function isCloseBracketToken(node?: ts.Node): 100 | node is ts.Token { 101 | return node != null && node.kind === ts.SyntaxKind.CloseBracketToken; 102 | } 103 | export function isDotToken(node?: ts.Node): 104 | node is ts.Token { 105 | return node != null && node.kind === ts.SyntaxKind.DotToken; 106 | } 107 | export function isDotDotDotToken(node?: ts.Node): 108 | node is ts.Token { 109 | return node != null && node.kind === ts.SyntaxKind.DotDotDotToken; 110 | } 111 | export function isSemicolonToken(node?: ts.Node): 112 | node is ts.Token { 113 | return node != null && node.kind === ts.SyntaxKind.SemicolonToken; 114 | } 115 | export function isCommaToken(node?: ts.Node): 116 | node is ts.Token { 117 | return node != null && node.kind === ts.SyntaxKind.CommaToken; 118 | } 119 | export function isLessThanToken(node?: ts.Node): 120 | node is ts.Token { 121 | return node != null && node.kind === ts.SyntaxKind.LessThanToken; 122 | } 123 | export function isLessThanSlashToken(node?: ts.Node): 124 | node is ts.Token { 125 | return node != null && node.kind === ts.SyntaxKind.LessThanSlashToken; 126 | } 127 | export function isGreaterThanToken(node?: ts.Node): 128 | node is ts.Token { 129 | return node != null && node.kind === ts.SyntaxKind.GreaterThanToken; 130 | } 131 | export function isLessThanEqualsToken(node?: ts.Node): 132 | node is ts.Token { 133 | return node != null && node.kind === ts.SyntaxKind.LessThanEqualsToken; 134 | } 135 | export function isGreaterThanEqualsToken(node?: ts.Node): 136 | node is ts.Token { 137 | return node != null && node.kind === ts.SyntaxKind.GreaterThanEqualsToken; 138 | } 139 | export function isEqualsEqualsToken(node?: ts.Node): 140 | node is ts.Token { 141 | return node != null && node.kind === ts.SyntaxKind.EqualsEqualsToken; 142 | } 143 | export function isExclamationEqualsToken(node?: ts.Node): 144 | node is ts.Token { 145 | return node != null && node.kind === ts.SyntaxKind.ExclamationEqualsToken; 146 | } 147 | export function isEqualsEqualsEqualsToken(node?: ts.Node): 148 | node is ts.Token { 149 | return node != null && node.kind === ts.SyntaxKind.EqualsEqualsEqualsToken; 150 | } 151 | export function isExclamationEqualsEqualsToken(node?: ts.Node): 152 | node is ts.Token { 153 | return node != null && node.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; 154 | } 155 | export function isEqualsGreaterThanToken(node?: ts.Node): 156 | node is ts.Token { 157 | return node != null && node.kind === ts.SyntaxKind.EqualsGreaterThanToken; 158 | } 159 | export function isPlusToken(node?: ts.Node): 160 | node is ts.Token { 161 | return node != null && node.kind === ts.SyntaxKind.PlusToken; 162 | } 163 | export function isMinusToken(node?: ts.Node): 164 | node is ts.Token { 165 | return node != null && node.kind === ts.SyntaxKind.MinusToken; 166 | } 167 | export function isAsteriskToken(node?: ts.Node): 168 | node is ts.Token { 169 | return node != null && node.kind === ts.SyntaxKind.AsteriskToken; 170 | } 171 | export function isAsteriskAsteriskToken(node?: ts.Node): 172 | node is ts.Token { 173 | return node != null && node.kind === ts.SyntaxKind.AsteriskAsteriskToken; 174 | } 175 | export function isSlashToken(node?: ts.Node): 176 | node is ts.Token { 177 | return node != null && node.kind === ts.SyntaxKind.SlashToken; 178 | } 179 | export function isPercentToken(node?: ts.Node): 180 | node is ts.Token { 181 | return node != null && node.kind === ts.SyntaxKind.PercentToken; 182 | } 183 | export function isPlusPlusToken(node?: ts.Node): 184 | node is ts.Token { 185 | return node != null && node.kind === ts.SyntaxKind.PlusPlusToken; 186 | } 187 | export function isMinusMinusToken(node?: ts.Node): 188 | node is ts.Token { 189 | return node != null && node.kind === ts.SyntaxKind.MinusMinusToken; 190 | } 191 | export function isLessThanLessThanToken(node?: ts.Node): 192 | node is ts.Token { 193 | return node != null && node.kind === ts.SyntaxKind.LessThanLessThanToken; 194 | } 195 | export function isGreaterThanGreaterThanToken(node?: ts.Node): 196 | node is ts.Token { 197 | return node != null && node.kind === ts.SyntaxKind.GreaterThanGreaterThanToken; 198 | } 199 | export function isGreaterThanGreaterThanGreaterThanToken(node?: ts.Node): 200 | node is ts.Token { 201 | return node != null && node.kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken; 202 | } 203 | export function isAmpersandToken(node?: ts.Node): 204 | node is ts.Token { 205 | return node != null && node.kind === ts.SyntaxKind.AmpersandToken; 206 | } 207 | export function isBarToken(node?: ts.Node): 208 | node is ts.Token { 209 | return node != null && node.kind === ts.SyntaxKind.BarToken; 210 | } 211 | export function isCaretToken(node?: ts.Node): 212 | node is ts.Token { 213 | return node != null && node.kind === ts.SyntaxKind.CaretToken; 214 | } 215 | export function isExclamationToken(node?: ts.Node): 216 | node is ts.Token { 217 | return node != null && node.kind === ts.SyntaxKind.ExclamationToken; 218 | } 219 | export function isTildeToken(node?: ts.Node): 220 | node is ts.Token { 221 | return node != null && node.kind === ts.SyntaxKind.TildeToken; 222 | } 223 | export function isAmpersandAmpersandToken(node?: ts.Node): 224 | node is ts.Token { 225 | return node != null && node.kind === ts.SyntaxKind.AmpersandAmpersandToken; 226 | } 227 | export function isBarBarToken(node?: ts.Node): 228 | node is ts.Token { 229 | return node != null && node.kind === ts.SyntaxKind.BarBarToken; 230 | } 231 | export function isQuestionToken(node?: ts.Node): 232 | node is ts.Token { 233 | return node != null && node.kind === ts.SyntaxKind.QuestionToken; 234 | } 235 | export function isColonToken(node?: ts.Node): 236 | node is ts.Token { 237 | return node != null && node.kind === ts.SyntaxKind.ColonToken; 238 | } 239 | export function isAtToken(node?: ts.Node): 240 | node is ts.Token { 241 | return node != null && node.kind === ts.SyntaxKind.AtToken; 242 | } 243 | export function isEqualsToken(node?: ts.Node): 244 | node is ts.Token { 245 | return node != null && node.kind === ts.SyntaxKind.EqualsToken; 246 | } 247 | export function isPlusEqualsToken(node?: ts.Node): 248 | node is ts.Token { 249 | return node != null && node.kind === ts.SyntaxKind.PlusEqualsToken; 250 | } 251 | export function isMinusEqualsToken(node?: ts.Node): 252 | node is ts.Token { 253 | return node != null && node.kind === ts.SyntaxKind.MinusEqualsToken; 254 | } 255 | export function isAsteriskEqualsToken(node?: ts.Node): 256 | node is ts.Token { 257 | return node != null && node.kind === ts.SyntaxKind.AsteriskEqualsToken; 258 | } 259 | export function isAsteriskAsteriskEqualsToken(node?: ts.Node): 260 | node is ts.Token { 261 | return node != null && node.kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken; 262 | } 263 | export function isSlashEqualsToken(node?: ts.Node): 264 | node is ts.Token { 265 | return node != null && node.kind === ts.SyntaxKind.SlashEqualsToken; 266 | } 267 | export function isPercentEqualsToken(node?: ts.Node): 268 | node is ts.Token { 269 | return node != null && node.kind === ts.SyntaxKind.PercentEqualsToken; 270 | } 271 | export function isLessThanLessThanEqualsToken(node?: ts.Node): 272 | node is ts.Token { 273 | return node != null && node.kind === ts.SyntaxKind.LessThanLessThanEqualsToken; 274 | } 275 | export function isGreaterThanGreaterThanEqualsToken(node?: ts.Node): 276 | node is ts.Token { 277 | return node != null && node.kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken; 278 | } 279 | export function isGreaterThanGreaterThanGreaterThanEqualsToken(node: ts.Node): 280 | node is 281 | ts.Token { 282 | return node.kind === 283 | ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; 284 | } 285 | export function isAmpersandEqualsToken(node?: ts.Node): 286 | node is ts.Token { 287 | return node != null && node.kind === ts.SyntaxKind.AmpersandEqualsToken; 288 | } 289 | export function isBarEqualsToken(node?: ts.Node): 290 | node is ts.Token { 291 | return node != null && node.kind === ts.SyntaxKind.BarEqualsToken; 292 | } 293 | export function isCaretEqualsToken(node?: ts.Node): 294 | node is ts.Token { 295 | return node != null && node.kind === ts.SyntaxKind.CaretEqualsToken; 296 | } 297 | export function isIdentifier(node?: ts.Node): node is ts.Identifier { 298 | return node != null && node.kind === ts.SyntaxKind.Identifier; 299 | } 300 | export function isQualifiedName(node?: ts.Node): node is ts.QualifiedName { 301 | return node != null && node.kind === ts.SyntaxKind.QualifiedName; 302 | } 303 | export function isComputedPropertyName(node?: ts.Node): 304 | node is ts.ComputedPropertyName { 305 | return node != null && node.kind === ts.SyntaxKind.ComputedPropertyName; 306 | } 307 | export function isTypeParameter(node?: ts.Node): 308 | node is ts.TypeParameterDeclaration { 309 | return node != null && node.kind === ts.SyntaxKind.TypeParameter; 310 | } 311 | export function isParameter(node?: ts.Node): node is ts.ParameterDeclaration { 312 | return node != null && node.kind === ts.SyntaxKind.Parameter; 313 | } 314 | export function isDecorator(node?: ts.Node): node is ts.Decorator { 315 | return node != null && node.kind === ts.SyntaxKind.Decorator; 316 | } 317 | export function isPropertySignature(node?: ts.Node): 318 | node is ts.PropertySignature { 319 | return node != null && node.kind === ts.SyntaxKind.PropertySignature; 320 | } 321 | export function isPropertyDeclaration(node?: ts.Node): 322 | node is ts.PropertyDeclaration { 323 | return node != null && node.kind === ts.SyntaxKind.PropertyDeclaration; 324 | } 325 | export function isMethodSignature(node?: ts.Node): node is ts.MethodSignature { 326 | return node != null && node.kind === ts.SyntaxKind.MethodSignature; 327 | } 328 | export function isMethodDeclaration(node?: ts.Node): 329 | node is ts.MethodDeclaration { 330 | return node != null && node.kind === ts.SyntaxKind.MethodDeclaration; 331 | } 332 | export function isConstructor(node?: ts.Node): 333 | node is ts.ConstructorDeclaration { 334 | return node != null && node.kind === ts.SyntaxKind.Constructor; 335 | } 336 | export function isGetAccessor(node?: ts.Node): 337 | node is ts.GetAccessorDeclaration { 338 | return node != null && node.kind === ts.SyntaxKind.GetAccessor; 339 | } 340 | export function isSetAccessor(node?: ts.Node): 341 | node is ts.SetAccessorDeclaration { 342 | return node != null && node.kind === ts.SyntaxKind.SetAccessor; 343 | } 344 | export function isCallSignature(node?: ts.Node): 345 | node is ts.CallSignatureDeclaration { 346 | return node != null && node.kind === ts.SyntaxKind.CallSignature; 347 | } 348 | export function isConstructSignature(node?: ts.Node): 349 | node is ts.ConstructSignatureDeclaration { 350 | return node != null && node.kind === ts.SyntaxKind.ConstructSignature; 351 | } 352 | export function isIndexSignature(node?: ts.Node): 353 | node is ts.IndexSignatureDeclaration { 354 | return node != null && node.kind === ts.SyntaxKind.IndexSignature; 355 | } 356 | export function isTypePredicate(node?: ts.Node): node is ts.TypePredicateNode { 357 | return node != null && node.kind === ts.SyntaxKind.TypePredicate; 358 | } 359 | export function isTypeReference(node?: ts.Node): node is ts.TypeReferenceNode { 360 | return node != null && node.kind === ts.SyntaxKind.TypeReference; 361 | } 362 | export function isFunctionType(node?: ts.Node): node is ts.FunctionTypeNode { 363 | return node != null && node.kind === ts.SyntaxKind.FunctionType; 364 | } 365 | export function isConstructorType(node?: ts.Node): 366 | node is ts.ConstructorTypeNode { 367 | return node != null && node.kind === ts.SyntaxKind.ConstructorType; 368 | } 369 | export function isTypeQuery(node?: ts.Node): node is ts.TypeQueryNode { 370 | return node != null && node.kind === ts.SyntaxKind.TypeQuery; 371 | } 372 | export function isTypeLiteral(node?: ts.Node): node is ts.TypeLiteralNode { 373 | return node != null && node.kind === ts.SyntaxKind.TypeLiteral; 374 | } 375 | export function isArrayType(node?: ts.Node): node is ts.ArrayTypeNode { 376 | return node != null && node.kind === ts.SyntaxKind.ArrayType; 377 | } 378 | export function isTupleType(node?: ts.Node): node is ts.TupleTypeNode { 379 | return node != null && node.kind === ts.SyntaxKind.TupleType; 380 | } 381 | export function isUnionType(node?: ts.Node): node is ts.UnionTypeNode { 382 | return node != null && node.kind === ts.SyntaxKind.UnionType; 383 | } 384 | export function isIntersectionType(node?: ts.Node): 385 | node is ts.IntersectionTypeNode { 386 | return node != null && node.kind === ts.SyntaxKind.IntersectionType; 387 | } 388 | export function isParenthesizedType(node?: ts.Node): 389 | node is ts.ParenthesizedTypeNode { 390 | return node != null && node.kind === ts.SyntaxKind.ParenthesizedType; 391 | } 392 | export function isThisType(node?: ts.Node): node is ts.ThisTypeNode { 393 | return node != null && node.kind === ts.SyntaxKind.ThisType; 394 | } 395 | export function isTypeOperator(node?: ts.Node): node is ts.TypeOperatorNode { 396 | return node != null && node.kind === ts.SyntaxKind.TypeOperator; 397 | } 398 | export function isIndexedAccessType(node?: ts.Node): 399 | node is ts.IndexedAccessTypeNode { 400 | return node != null && node.kind === ts.SyntaxKind.IndexedAccessType; 401 | } 402 | export function isMappedType(node?: ts.Node): node is ts.MappedTypeNode { 403 | return node != null && node.kind === ts.SyntaxKind.MappedType; 404 | } 405 | export function isLiteralType(node?: ts.Node): node is ts.LiteralTypeNode { 406 | return node != null && node.kind === ts.SyntaxKind.LiteralType; 407 | } 408 | export function isObjectBindingPattern(node?: ts.Node): 409 | node is ts.ObjectBindingPattern { 410 | return node != null && node.kind === ts.SyntaxKind.ObjectBindingPattern; 411 | } 412 | export function isArrayBindingPattern(node?: ts.Node): 413 | node is ts.ArrayBindingPattern { 414 | return node != null && node.kind === ts.SyntaxKind.ArrayBindingPattern; 415 | } 416 | export function isBindingElement(node?: ts.Node): node is ts.BindingElement { 417 | return node != null && node.kind === ts.SyntaxKind.BindingElement; 418 | } 419 | export function isArrayLiteralExpression(node?: ts.Node): 420 | node is ts.ArrayLiteralExpression { 421 | return node != null && node.kind === ts.SyntaxKind.ArrayLiteralExpression; 422 | } 423 | export function isObjectLiteralExpression(node?: ts.Node): 424 | node is ts.ObjectLiteralExpression { 425 | return node != null && node.kind === ts.SyntaxKind.ObjectLiteralExpression; 426 | } 427 | export function isPropertyAccessExpression(node?: ts.Node): 428 | node is ts.PropertyAccessExpression { 429 | return node != null && node.kind === ts.SyntaxKind.PropertyAccessExpression; 430 | } 431 | export function isElementAccessExpression(node?: ts.Node): 432 | node is ts.ElementAccessExpression { 433 | return node != null && node.kind === ts.SyntaxKind.ElementAccessExpression; 434 | } 435 | export function isCallExpression(node?: ts.Node): node is ts.CallExpression { 436 | return node != null && node.kind === ts.SyntaxKind.CallExpression; 437 | } 438 | export function isNewExpression(node?: ts.Node): node is ts.NewExpression { 439 | return node != null && node.kind === ts.SyntaxKind.NewExpression; 440 | } 441 | export function isTaggedTemplateExpression(node?: ts.Node): 442 | node is ts.TaggedTemplateExpression { 443 | return node != null && node.kind === ts.SyntaxKind.TaggedTemplateExpression; 444 | } 445 | // export function isTypeAssertionExpression(node: ts.Node): node is 446 | // ts.TypeAssertionExpression { 447 | // return node.kind === ts.SyntaxKind.TypeAssertionExpression; 448 | // } 449 | export function isParenthesizedExpression(node?: ts.Node): 450 | node is ts.ParenthesizedExpression { 451 | return node != null && node.kind === ts.SyntaxKind.ParenthesizedExpression; 452 | } 453 | export function isFunctionExpression(node?: ts.Node): 454 | node is ts.FunctionExpression { 455 | return node != null && node.kind === ts.SyntaxKind.FunctionExpression; 456 | } 457 | export function isArrowFunction(node?: ts.Node): node is ts.ArrowFunction { 458 | return node != null && node.kind === ts.SyntaxKind.ArrowFunction; 459 | } 460 | export function isDeleteExpression(node?: ts.Node): node is ts.DeleteExpression { 461 | return node != null && node.kind === ts.SyntaxKind.DeleteExpression; 462 | } 463 | export function isTypeOfExpression(node?: ts.Node): node is ts.TypeOfExpression { 464 | return node != null && node.kind === ts.SyntaxKind.TypeOfExpression; 465 | } 466 | export function isVoidExpression(node?: ts.Node): node is ts.VoidExpression { 467 | return node != null && node.kind === ts.SyntaxKind.VoidExpression; 468 | } 469 | export function isAwaitExpression(node?: ts.Node): node is ts.AwaitExpression { 470 | return node != null && node.kind === ts.SyntaxKind.AwaitExpression; 471 | } 472 | export function isPrefixUnaryExpression(node?: ts.Node): 473 | node is ts.PrefixUnaryExpression { 474 | return node != null && node.kind === ts.SyntaxKind.PrefixUnaryExpression; 475 | } 476 | export function isPostfixUnaryExpression(node?: ts.Node): 477 | node is ts.PostfixUnaryExpression { 478 | return node != null && node.kind === ts.SyntaxKind.PostfixUnaryExpression; 479 | } 480 | export function isBinaryExpression(node?: ts.Node): node is ts.BinaryExpression { 481 | return node != null && node.kind === ts.SyntaxKind.BinaryExpression; 482 | } 483 | export function isConditionalExpression(node?: ts.Node): 484 | node is ts.ConditionalExpression { 485 | return node != null && node.kind === ts.SyntaxKind.ConditionalExpression; 486 | } 487 | export function isTemplateExpression(node?: ts.Node): 488 | node is ts.TemplateExpression { 489 | return node != null && node.kind === ts.SyntaxKind.TemplateExpression; 490 | } 491 | export function isYieldExpression(node?: ts.Node): node is ts.YieldExpression { 492 | return node != null && node.kind === ts.SyntaxKind.YieldExpression; 493 | } 494 | export function isSpreadElement(node?: ts.Node): node is ts.SpreadElement { 495 | return node != null && node.kind === ts.SyntaxKind.SpreadElement; 496 | } 497 | export function isClassExpression(node?: ts.Node): node is ts.ClassExpression { 498 | return node != null && node.kind === ts.SyntaxKind.ClassExpression; 499 | } 500 | export function isOmittedExpression(node?: ts.Node): 501 | node is ts.OmittedExpression { 502 | return node != null && node.kind === ts.SyntaxKind.OmittedExpression; 503 | } 504 | export function isExpressionWithTypeArguments(node?: ts.Node): 505 | node is ts.ExpressionWithTypeArguments { 506 | return node != null && node.kind === ts.SyntaxKind.ExpressionWithTypeArguments; 507 | } 508 | export function isAsExpression(node?: ts.Node): node is ts.AsExpression { 509 | return node != null && node.kind === ts.SyntaxKind.AsExpression; 510 | } 511 | export function isNonNullExpression(node?: ts.Node): 512 | node is ts.NonNullExpression { 513 | return node != null && node.kind === ts.SyntaxKind.NonNullExpression; 514 | } 515 | export function isTemplateSpan(node?: ts.Node): node is ts.TemplateSpan { 516 | return node != null && node.kind === ts.SyntaxKind.TemplateSpan; 517 | } 518 | export function isSemicolonClassElement(node?: ts.Node): 519 | node is ts.SemicolonClassElement { 520 | return node != null && node.kind === ts.SyntaxKind.SemicolonClassElement; 521 | } 522 | export function isBlock(node?: ts.Node): node is ts.Block { 523 | return node != null && node.kind === ts.SyntaxKind.Block; 524 | } 525 | export function isVariableStatement(node?: ts.Node): 526 | node is ts.VariableStatement { 527 | return node != null && node.kind === ts.SyntaxKind.VariableStatement; 528 | } 529 | export function isEmptyStatement(node?: ts.Node): node is ts.EmptyStatement { 530 | return node != null && node.kind === ts.SyntaxKind.EmptyStatement; 531 | } 532 | export function isExpressionStatement(node?: ts.Node): 533 | node is ts.ExpressionStatement { 534 | return node != null && node.kind === ts.SyntaxKind.ExpressionStatement; 535 | } 536 | export function isIfStatement(node?: ts.Node): node is ts.IfStatement { 537 | return node != null && node.kind === ts.SyntaxKind.IfStatement; 538 | } 539 | export function isDoStatement(node?: ts.Node): node is ts.DoStatement { 540 | return node != null && node.kind === ts.SyntaxKind.DoStatement; 541 | } 542 | export function isWhileStatement(node?: ts.Node): node is ts.WhileStatement { 543 | return node != null && node.kind === ts.SyntaxKind.WhileStatement; 544 | } 545 | export function isForStatement(node?: ts.Node): node is ts.ForStatement { 546 | return node != null && node.kind === ts.SyntaxKind.ForStatement; 547 | } 548 | export function isForInStatement(node?: ts.Node): node is ts.ForInStatement { 549 | return node != null && node.kind === ts.SyntaxKind.ForInStatement; 550 | } 551 | export function isForOfStatement(node?: ts.Node): node is ts.ForOfStatement { 552 | return node != null && node.kind === ts.SyntaxKind.ForOfStatement; 553 | } 554 | export function isContinueStatement(node?: ts.Node): 555 | node is ts.ContinueStatement { 556 | return node != null && node.kind === ts.SyntaxKind.ContinueStatement; 557 | } 558 | export function isBreakStatement(node?: ts.Node): node is ts.BreakStatement { 559 | return node != null && node.kind === ts.SyntaxKind.BreakStatement; 560 | } 561 | export function isReturnStatement(node?: ts.Node): node is ts.ReturnStatement { 562 | return node != null && node.kind === ts.SyntaxKind.ReturnStatement; 563 | } 564 | export function isWithStatement(node?: ts.Node): node is ts.WithStatement { 565 | return node != null && node.kind === ts.SyntaxKind.WithStatement; 566 | } 567 | export function isSwitchStatement(node?: ts.Node): node is ts.SwitchStatement { 568 | return node != null && node.kind === ts.SyntaxKind.SwitchStatement; 569 | } 570 | export function isLabeledStatement(node?: ts.Node): node is ts.LabeledStatement { 571 | return node != null && node.kind === ts.SyntaxKind.LabeledStatement; 572 | } 573 | export function isThrowStatement(node?: ts.Node): node is ts.ThrowStatement { 574 | return node != null && node.kind === ts.SyntaxKind.ThrowStatement; 575 | } 576 | export function isTryStatement(node?: ts.Node): node is ts.TryStatement { 577 | return node != null && node.kind === ts.SyntaxKind.TryStatement; 578 | } 579 | export function isDebuggerStatement(node?: ts.Node): 580 | node is ts.DebuggerStatement { 581 | return node != null && node.kind === ts.SyntaxKind.DebuggerStatement; 582 | } 583 | export function isVariableDeclaration(node?: ts.Node): 584 | node is ts.VariableDeclaration { 585 | return node != null && node.kind === ts.SyntaxKind.VariableDeclaration; 586 | } 587 | export function isVariableDeclarationList(node?: ts.Node): 588 | node is ts.VariableDeclarationList { 589 | return node != null && node.kind === ts.SyntaxKind.VariableDeclarationList; 590 | } 591 | export function isFunctionDeclaration(node?: ts.Node): 592 | node is ts.FunctionDeclaration { 593 | return node != null && node.kind === ts.SyntaxKind.FunctionDeclaration; 594 | } 595 | export function isClassDeclaration(node?: ts.Node): node is ts.ClassDeclaration { 596 | return node != null && node.kind === ts.SyntaxKind.ClassDeclaration; 597 | } 598 | export function isInterfaceDeclaration(node?: ts.Node): 599 | node is ts.InterfaceDeclaration { 600 | return node != null && node.kind === ts.SyntaxKind.InterfaceDeclaration; 601 | } 602 | export function isTypeAliasDeclaration(node?: ts.Node): 603 | node is ts.TypeAliasDeclaration { 604 | return node != null && node.kind === ts.SyntaxKind.TypeAliasDeclaration; 605 | } 606 | export function isEnumDeclaration(node?: ts.Node): node is ts.EnumDeclaration { 607 | return node != null && node.kind === ts.SyntaxKind.EnumDeclaration; 608 | } 609 | export function isModuleDeclaration(node?: ts.Node): 610 | node is ts.ModuleDeclaration { 611 | return node != null && node.kind === ts.SyntaxKind.ModuleDeclaration; 612 | } 613 | export function isModuleBlock(node?: ts.Node): node is ts.ModuleBlock { 614 | return node != null && node.kind === ts.SyntaxKind.ModuleBlock; 615 | } 616 | export function isCaseBlock(node?: ts.Node): node is ts.CaseBlock { 617 | return node != null && node.kind === ts.SyntaxKind.CaseBlock; 618 | } 619 | export function isNamespaceExportDeclaration(node?: ts.Node): 620 | node is ts.NamespaceExportDeclaration { 621 | return node != null && node.kind === ts.SyntaxKind.NamespaceExportDeclaration; 622 | } 623 | export function isImportEqualsDeclaration(node?: ts.Node): 624 | node is ts.ImportEqualsDeclaration { 625 | return node != null && node.kind === ts.SyntaxKind.ImportEqualsDeclaration; 626 | } 627 | export function isImportDeclaration(node?: ts.Node): 628 | node is ts.ImportDeclaration { 629 | return node != null && node.kind === ts.SyntaxKind.ImportDeclaration; 630 | } 631 | export function isImportClause(node?: ts.Node): node is ts.ImportClause { 632 | return node != null && node.kind === ts.SyntaxKind.ImportClause; 633 | } 634 | export function isNamespaceImport(node?: ts.Node): node is ts.NamespaceImport { 635 | return node != null && node.kind === ts.SyntaxKind.NamespaceImport; 636 | } 637 | export function isNamedImports(node?: ts.Node): node is ts.NamedImports { 638 | return node != null && node.kind === ts.SyntaxKind.NamedImports; 639 | } 640 | export function isImportSpecifier(node?: ts.Node): node is ts.ImportSpecifier { 641 | return node != null && node.kind === ts.SyntaxKind.ImportSpecifier; 642 | } 643 | export function isExportAssignment(node?: ts.Node): node is ts.ExportAssignment { 644 | return node != null && node.kind === ts.SyntaxKind.ExportAssignment; 645 | } 646 | export function isExportDeclaration(node?: ts.Node): 647 | node is ts.ExportDeclaration { 648 | return node != null && node.kind === ts.SyntaxKind.ExportDeclaration; 649 | } 650 | export function isNamedExports(node?: ts.Node): node is ts.NamedExports { 651 | return node != null && node.kind === ts.SyntaxKind.NamedExports; 652 | } 653 | export function isExportSpecifier(node?: ts.Node): node is ts.ExportSpecifier { 654 | return node != null && node.kind === ts.SyntaxKind.ExportSpecifier; 655 | } 656 | export function isMissingDeclaration(node?: ts.Node): 657 | node is ts.MissingDeclaration { 658 | return node != null && node.kind === ts.SyntaxKind.MissingDeclaration; 659 | } 660 | export function isExternalModuleReference(node?: ts.Node): 661 | node is ts.ExternalModuleReference { 662 | return node != null && node.kind === ts.SyntaxKind.ExternalModuleReference; 663 | } 664 | export function isJsxElement(node?: ts.Node): node is ts.JsxElement { 665 | return node != null && node.kind === ts.SyntaxKind.JsxElement; 666 | } 667 | export function isJsxSelfClosingElement(node?: ts.Node): 668 | node is ts.JsxSelfClosingElement { 669 | return node != null && node.kind === ts.SyntaxKind.JsxSelfClosingElement; 670 | } 671 | export function isJsxOpeningElement(node?: ts.Node): 672 | node is ts.JsxOpeningElement { 673 | return node != null && node.kind === ts.SyntaxKind.JsxOpeningElement; 674 | } 675 | export function isJsxClosingElement(node?: ts.Node): 676 | node is ts.JsxClosingElement { 677 | return node != null && node.kind === ts.SyntaxKind.JsxClosingElement; 678 | } 679 | export function isJsxAttribute(node?: ts.Node): node is ts.JsxAttribute { 680 | return node != null && node.kind === ts.SyntaxKind.JsxAttribute; 681 | } 682 | export function isJsxSpreadAttribute(node?: ts.Node): 683 | node is ts.JsxSpreadAttribute { 684 | return node != null && node.kind === ts.SyntaxKind.JsxSpreadAttribute; 685 | } 686 | export function isJsxExpression(node?: ts.Node): node is ts.JsxExpression { 687 | return node != null && node.kind === ts.SyntaxKind.JsxExpression; 688 | } 689 | export function isCaseClause(node?: ts.Node): node is ts.CaseClause { 690 | return node != null && node.kind === ts.SyntaxKind.CaseClause; 691 | } 692 | export function isDefaultClause(node?: ts.Node): node is ts.DefaultClause { 693 | return node != null && node.kind === ts.SyntaxKind.DefaultClause; 694 | } 695 | export function isHeritageClause(node?: ts.Node): node is ts.HeritageClause { 696 | return node != null && node.kind === ts.SyntaxKind.HeritageClause; 697 | } 698 | export function isCatchClause(node?: ts.Node): node is ts.CatchClause { 699 | return node != null && node.kind === ts.SyntaxKind.CatchClause; 700 | } 701 | export function isPropertyAssignment(node?: ts.Node): 702 | node is ts.PropertyAssignment { 703 | return node != null && node.kind === ts.SyntaxKind.PropertyAssignment; 704 | } 705 | export function isShorthandPropertyAssignment(node?: ts.Node): 706 | node is ts.ShorthandPropertyAssignment { 707 | return node != null && node.kind === ts.SyntaxKind.ShorthandPropertyAssignment; 708 | } 709 | export function isSpreadAssignment(node?: ts.Node): node is ts.SpreadAssignment { 710 | return node != null && node.kind === ts.SyntaxKind.SpreadAssignment; 711 | } 712 | export function isEnumMember(node?: ts.Node): node is ts.EnumMember { 713 | return node != null && node.kind === ts.SyntaxKind.EnumMember; 714 | } 715 | export function isSourceFile(node?: ts.Node): node is ts.SourceFile { 716 | return node != null && node.kind === ts.SyntaxKind.SourceFile; 717 | } 718 | export function isJSDocTypeExpression(node?: ts.Node): 719 | node is ts.JSDocTypeExpression { 720 | return node != null && node.kind === ts.SyntaxKind.JSDocTypeExpression; 721 | } 722 | export function isJSDocAllType(node?: ts.Node): node is ts.JSDocAllType { 723 | return node != null && node.kind === ts.SyntaxKind.JSDocAllType; 724 | } 725 | export function isJSDocUnknownType(node?: ts.Node): node is ts.JSDocUnknownType { 726 | return node != null && node.kind === ts.SyntaxKind.JSDocUnknownType; 727 | } 728 | export function isJSDocArrayType(node?: ts.Node): node is ts.JSDocArrayType { 729 | return node != null && node.kind === ts.SyntaxKind.JSDocArrayType; 730 | } 731 | export function isJSDocUnionType(node?: ts.Node): node is ts.JSDocUnionType { 732 | return node != null && node.kind === ts.SyntaxKind.JSDocUnionType; 733 | } 734 | export function isJSDocTupleType(node?: ts.Node): node is ts.JSDocTupleType { 735 | return node != null && node.kind === ts.SyntaxKind.JSDocTupleType; 736 | } 737 | export function isJSDocNullableType(node?: ts.Node): 738 | node is ts.JSDocNullableType { 739 | return node != null && node.kind === ts.SyntaxKind.JSDocNullableType; 740 | } 741 | export function isJSDocNonNullableType(node?: ts.Node): 742 | node is ts.JSDocNonNullableType { 743 | return node != null && node.kind === ts.SyntaxKind.JSDocNonNullableType; 744 | } 745 | export function isJSDocRecordType(node?: ts.Node): node is ts.JSDocRecordType { 746 | return node != null && node.kind === ts.SyntaxKind.JSDocRecordType; 747 | } 748 | export function isJSDocRecordMember(node?: ts.Node): 749 | node is ts.JSDocRecordMember { 750 | return node != null && node.kind === ts.SyntaxKind.JSDocRecordMember; 751 | } 752 | export function isJSDocTypeReference(node?: ts.Node): 753 | node is ts.JSDocTypeReference { 754 | return node != null && node.kind === ts.SyntaxKind.JSDocTypeReference; 755 | } 756 | export function isJSDocOptionalType(node?: ts.Node): 757 | node is ts.JSDocOptionalType { 758 | return node != null && node.kind === ts.SyntaxKind.JSDocOptionalType; 759 | } 760 | export function isJSDocFunctionType(node?: ts.Node): 761 | node is ts.JSDocFunctionType { 762 | return node != null && node.kind === ts.SyntaxKind.JSDocFunctionType; 763 | } 764 | export function isJSDocVariadicType(node?: ts.Node): 765 | node is ts.JSDocVariadicType { 766 | return node != null && node.kind === ts.SyntaxKind.JSDocVariadicType; 767 | } 768 | export function isJSDocConstructorType(node?: ts.Node): 769 | node is ts.JSDocConstructorType { 770 | return node != null && node.kind === ts.SyntaxKind.JSDocConstructorType; 771 | } 772 | export function isJSDocThisType(node?: ts.Node): node is ts.JSDocThisType { 773 | return node != null && node.kind === ts.SyntaxKind.JSDocThisType; 774 | } 775 | // export function isJSDocComment(node?: ts.Node): node is ts.JSDocComment { 776 | // return node != null && node.kind === ts.SyntaxKind.JSDocComment; 777 | // } 778 | export function isJSDocTag(node?: ts.Node): node is ts.JSDocTag { 779 | return node != null && node.kind === ts.SyntaxKind.JSDocTag; 780 | } 781 | export function isJSDocAugmentsTag(node?: ts.Node): node is ts.JSDocAugmentsTag { 782 | return node != null && node.kind === ts.SyntaxKind.JSDocAugmentsTag; 783 | } 784 | export function isJSDocParameterTag(node?: ts.Node): 785 | node is ts.JSDocParameterTag { 786 | return node != null && node.kind === ts.SyntaxKind.JSDocParameterTag; 787 | } 788 | export function isJSDocReturnTag(node?: ts.Node): node is ts.JSDocReturnTag { 789 | return node != null && node.kind === ts.SyntaxKind.JSDocReturnTag; 790 | } 791 | export function isJSDocTypeTag(node?: ts.Node): node is ts.JSDocTypeTag { 792 | return node != null && node.kind === ts.SyntaxKind.JSDocTypeTag; 793 | } 794 | export function isJSDocTemplateTag(node?: ts.Node): node is ts.JSDocTemplateTag { 795 | return node != null && node.kind === ts.SyntaxKind.JSDocTemplateTag; 796 | } 797 | export function isJSDocTypedefTag(node?: ts.Node): node is ts.JSDocTypedefTag { 798 | return node != null && node.kind === ts.SyntaxKind.JSDocTypedefTag; 799 | } 800 | export function isJSDocPropertyTag(node?: ts.Node): node is ts.JSDocPropertyTag { 801 | return node != null && node.kind === ts.SyntaxKind.JSDocPropertyTag; 802 | } 803 | export function isJSDocTypeLiteral(node?: ts.Node): node is ts.JSDocTypeLiteral { 804 | return node != null && node.kind === ts.SyntaxKind.JSDocTypeLiteral; 805 | } 806 | export function isJSDocLiteralType(node?: ts.Node): node is ts.JSDocLiteralType { 807 | return node != null && node.kind === ts.SyntaxKind.JSDocLiteralType; 808 | } 809 | export function isThisKeyword(node?: ts.Node): node is ts.Token { 810 | return node != null && node.kind === ts.SyntaxKind.ThisKeyword; 811 | } 812 | // export function isJSDocNullKeyword(node: ts.Node): node is 813 | // ts.JSDocNullKeyword { 814 | // return node.kind === ts.SyntaxKind.JSDocNullKeyword; 815 | // } 816 | // export function isJSDocUndefinedKeyword(node: ts.Node): node is 817 | // ts.JSDocUndefinedKeyword { 818 | // return node.kind === ts.SyntaxKind.JSDocUndefinedKeyword; 819 | // } 820 | // export function isJSDocNeverKeyword(node: ts.Node): node is 821 | // ts.JSDocNeverKeyword { 822 | // return node.kind === ts.SyntaxKind.JSDocNeverKeyword; 823 | // } 824 | export function isSyntaxList(node?: ts.Node): node is ts.SyntaxList { 825 | return node != null && node.kind === ts.SyntaxKind.SyntaxList; 826 | } 827 | 828 | export function isAbstractKeyword(node?: ts.Node): node is ts.Token { 829 | return node != null && node.kind === ts.SyntaxKind.AbstractKeyword; 830 | } 831 | export function isAsyncKeyword(node?: ts.Node): node is ts.Token { 832 | return node != null && node.kind === ts.SyntaxKind.AsyncKeyword; 833 | } 834 | export function isConstKeyword(node?: ts.Node): node is ts.Token { 835 | return node != null && node.kind === ts.SyntaxKind.ConstKeyword; 836 | } 837 | export function isDeclareKeyword(node?: ts.Node): node is ts.Token { 838 | return node != null && node.kind === ts.SyntaxKind.DeclareKeyword; 839 | } 840 | export function isDefaultKeyword(node?: ts.Node): node is ts.Token { 841 | return node != null && node.kind === ts.SyntaxKind.DefaultKeyword; 842 | } 843 | export function isExportKeyword(node?: ts.Node): node is ts.Token { 844 | return node != null && node.kind === ts.SyntaxKind.ExportKeyword; 845 | } 846 | export function isPublicKeyword(node?: ts.Node): node is ts.Token { 847 | return node != null && node.kind === ts.SyntaxKind.PublicKeyword; 848 | } 849 | export function isPrivateKeyword(node?: ts.Node): node is ts.Token { 850 | return node != null && node.kind === ts.SyntaxKind.PrivateKeyword; 851 | } 852 | export function isProtectedKeyword(node?: ts.Node): node is ts.Token { 853 | return node != null && node.kind === ts.SyntaxKind.ProtectedKeyword; 854 | } 855 | export function isReadonlyKeyword(node?: ts.Node): node is ts.Token { 856 | return node != null && node.kind === ts.SyntaxKind.ReadonlyKeyword; 857 | } 858 | export function isStaticKeyword(node?: ts.Node): node is ts.Token { 859 | return node != null && node.kind === ts.SyntaxKind.StaticKeyword; 860 | } 861 | 862 | // export function isNotEmittedStatement(node: ts.Node): node is 863 | // ts.NotEmittedStatement { 864 | // return node.kind === ts.SyntaxKind.NotEmittedStatement; 865 | // } 866 | // export function isPartiallyEmittedExpression(node: ts.Node): node is 867 | // ts.PartiallyEmittedExpression { 868 | // return node.kind === ts.SyntaxKind.PartiallyEmittedExpression; 869 | // } 870 | // export function isMergeDeclarationMarker(node: ts.Node): node is 871 | // ts.MergeDeclarationMarker { 872 | // return node.kind === ts.SyntaxKind.MergeDeclarationMarker; 873 | // } 874 | // export function isEndOfDeclarationMarker(node: ts.Node): node is 875 | // ts.EndOfDeclarationMarker { 876 | // return node.kind === ts.SyntaxKind.EndOfDeclarationMarker; 877 | // } 878 | // export function isCount(node?: ts.Node): node is ts.Count { 879 | // return node != null && node.kind === ts.SyntaxKind.Count; 880 | // } 881 | // export function isFirstAssignment(node?: ts. != null && .Node): node is ts.FirstAssignment 882 | // { 883 | // return node.kind === ts.SyntaxKind.FirstAssignment; 884 | // } 885 | // export function isLastAssignment(node?: ts.Node): node is ts.LastAssignment { 886 | // return node != null && node.kind === ts.SyntaxKind.LastAssignment; 887 | // } 888 | // export function isFirstCompoundAssignment(node: ts.Node): node is 889 | // ts.FirstCompoundAssignment { 890 | // return node.kind === ts.SyntaxKind.FirstCompoundAssignment; 891 | // } 892 | // export function isLastCompoundAssignment(node: ts.Node): node is 893 | // ts.LastCompoundAssignment { 894 | // return node.kind === ts.SyntaxKind.LastCompoundAssignment; 895 | // } 896 | // export function isFirstReservedWord(node: ts.Node): node is 897 | // ts.FirstReservedWord { 898 | // return node.kind === ts.SyntaxKind.FirstReservedWord; 899 | // } 900 | // export function isLastReservedWord(node: ts.Node): node is 901 | // ts.LastReservedWord { 902 | // return node.kind === ts.SyntaxKind.LastReservedWord; 903 | // } 904 | // export function isFirstKeyword(node?: ts.Node): node is ts.FirstKeyword { 905 | // return node != null && node.kind === ts.SyntaxKind.FirstKeyword; 906 | // } 907 | // export function isLastKeyword(node?: ts.Node): node is ts.LastKeyword { 908 | // return node != null && node.kind === ts.SyntaxKind.LastKeyword; 909 | // } 910 | // export function isFirstFutureReservedWord(node: ts.Node): node is 911 | // ts.FirstFutureReservedWord { 912 | // return node.kind === ts.SyntaxKind.FirstFutureReservedWord; 913 | // } 914 | // export function isLastFutureReservedWord(node: ts.Node): node is 915 | // ts.LastFutureReservedWord { 916 | // return node.kind === ts.SyntaxKind.LastFutureReservedWord; 917 | // } 918 | // export function isFirstTypeNode(node?: ts.Node): node is ts.FirstTypeNode { 919 | // return node != null && node.kind === ts.SyntaxKind.FirstTypeNode; 920 | // } 921 | // export function isLastTypeNode(node?: ts.Node): node is ts.LastTypeNode { 922 | // return node != null && node.kind === ts.SyntaxKind.LastTypeNode; 923 | // } 924 | // export function isFirstPunctuation(node: ts.Node): node is 925 | // ts.FirstPunctuation { 926 | // return node.kind === ts.SyntaxKind.FirstPunctuation; 927 | // } 928 | // export function isLastPunctuation(node?: ts. != null && .Node): node is ts.LastPunctuation 929 | // { 930 | // return node.kind === ts.SyntaxKind.LastPunctuation; 931 | // } 932 | 933 | export function isVarKeyword(node?: ts.Node): 934 | node is ts.Token { 935 | return node != null && node.kind === ts.SyntaxKind.VarKeyword; 936 | } 937 | export function isInKeyword(node?: ts.Node): 938 | node is ts.Token { 939 | return node != null && node.kind === ts.SyntaxKind.InKeyword; 940 | } 941 | export function isFirstToken(node?: ts.Node): 942 | node is ts.Token { 943 | return node != null && node.kind === ts.SyntaxKind.FirstToken; 944 | } 945 | export function isLastToken(node?: ts.Node): 946 | node is ts.Token { 947 | return node != null && node.kind === ts.SyntaxKind.LastToken; 948 | } 949 | export function isFirstTriviaToken(node?: ts.Node): 950 | node is ts.Token { 951 | return node != null && node.kind === ts.SyntaxKind.FirstTriviaToken; 952 | } 953 | export function isLastTriviaToken(node?: ts.Node): 954 | node is ts.Token { 955 | return node != null && node.kind === ts.SyntaxKind.LastTriviaToken; 956 | } 957 | export function isFirstLiteralToken(node?: ts.Node): 958 | node is ts.Token { 959 | return node != null && node.kind === ts.SyntaxKind.FirstLiteralToken; 960 | } 961 | export function isLastLiteralToken(node?: ts.Node): 962 | node is ts.Token { 963 | return node != null && node.kind === ts.SyntaxKind.LastLiteralToken; 964 | } 965 | export function isFirstTemplateToken(node?: ts.Node): 966 | node is ts.Token { 967 | return node != null && node.kind === ts.SyntaxKind.FirstTemplateToken; 968 | } 969 | export function isLastTemplateToken(node?: ts.Node): 970 | node is ts.Token { 971 | return node != null && node.kind === ts.SyntaxKind.LastTemplateToken; 972 | } 973 | // export function isFirstBinaryOperator(node: ts.Node): node is 974 | // ts.FirstBinaryOperator { 975 | // return node.kind === ts.SyntaxKind.FirstBinaryOperator; 976 | // } 977 | // export function isLastBinaryOperator(node: ts.Node): node is 978 | // ts.LastBinaryOperator { 979 | // return node.kind === ts.SyntaxKind.LastBinaryOperator; 980 | // } 981 | // export function isFirstNode(node?: ts.Node): node is ts.FirstNode { 982 | // return node != null && node.kind === ts.SyntaxKind.FirstNode; 983 | // } 984 | // export function isFirstJSDocNode(node?: ts.Node): node is ts.FirstJSDocNode { 985 | // return node != null && node.kind === ts.SyntaxKind.FirstJSDocNode; 986 | // } 987 | // export function isLastJSDocNode(node?: ts.Node): node is ts.LastJSDocNode { 988 | // return node != null && node.kind === ts.SyntaxKind.LastJSDocNode; 989 | // } 990 | // export function isFirstJSDocTagNode(node: ts.Node): node is 991 | // ts.FirstJSDocTagNode { 992 | // return node.kind === ts.SyntaxKind.FirstJSDocTagNode; 993 | // } 994 | // export function isLastJSDocTagNode(node: ts.Node): node is 995 | // ts.LastJSDocTagNode { 996 | // return node.kind === ts.SyntaxKind.LastJSDocTagNode; 997 | // } --------------------------------------------------------------------------------