├── transforms └── strict-type-args │ ├── .flowconfig │ ├── strict-type-args.js │ ├── README.md │ └── src │ └── strict-type-args.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE └── README.md /transforms/strict-type-args/.flowconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read the [full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to flow-codemod 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Contributor License Agreement ("CLA") 6 | In order to accept your pull request, we need you to submit a CLA. You only need 7 | to do this once to work on any of Facebook's open source projects. 8 | 9 | Complete your CLA here: 10 | 11 | ## Issues 12 | We use GitHub issues to track public bugs. Please ensure your description is 13 | clear and has sufficient instructions to be able to reproduce the issue. 14 | 15 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 16 | disclosure of security bugs. In those cases, please go through the process 17 | outlined on that page and do not file a public issue. 18 | 19 | ## License 20 | By contributing to flow-codemod, you agree that your contributions will be licensed 21 | under the LICENSE file in the root directory of this source tree. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flow-codemod 2 | 3 | This repository contains a collection of codemod scripts for use with 4 | [JSCodeshift](https://github.com/facebook/jscodeshift) that help update 5 | Flowified JS code. 6 | 7 | ### Setup & Run 8 | 9 | * `npm install -g jscodeshift` 10 | * `git clone https://github.com/flowtype/flow-codemod.git` 11 | * `jscodeshift -t ` 12 | (but note that individual transforms may require additional options, as documented) 13 | * Use the `-d` option for a dry-run and use `-p` to print the output for comparison 14 | 15 | ##### KNOWN ISSUES 16 | 17 | * jscodeshift currently uses Babel 5, which fails to parse certain JS idioms. 18 | Files that fail to parse will not be transformed, unfortunately. 19 | 20 | ### Included Scripts 21 | 22 | The following codemods can be found under the `transforms` directory: 23 | 24 | #### `strict-type-args` 25 | 26 | Adds explicit arguments to polymorphic type application expressions, 27 | based on errors from Flow. For example, 28 | 29 | ``` 30 | let map: Map = ... 31 | ``` 32 | 33 | ...becomes 34 | 35 | ``` 36 | let map: Map = ... 37 | ``` 38 | 39 | This prepares code for an upcoming change to strict type argument processing. For instructions and more info, see documentation in the transform subdirectory. 40 | 41 | ## License 42 | 43 | flow-codemod is MIT licensed, as found in the [LICENSE](LICENSE) file. 44 | -------------------------------------------------------------------------------- /transforms/strict-type-args/strict-type-args.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * Add explicit arguments to polymorphic type application expressions, 8 | * based on errors from Flow >= 0.25. 9 | * 10 | * See ../README.md for instructions. 11 | * 12 | * 13 | */ 14 | 15 | 'use-strict'; 16 | 17 | // minimal API type info in lieu of full libdef 18 | // 19 | 20 | // extract base filename from given path 21 | 22 | 23 | // loaded error info 24 | const getFileName = path => path.replace(/^.*[\\\/]/, ''); 25 | 26 | // load arity errors from given file (once per worker), return accessors 27 | // 28 | 29 | 30 | // accessors for loaded errors 31 | const loadArityErrors = function () { 32 | 33 | let lastErrorFileLoad = null; 34 | 35 | const re = /Application of polymorphic type needs 1 && messages[1].descr.match(re); 39 | return match ? { 40 | path: messages[0].path, 41 | start: parseInt(messages[0].loc.start.offset, 10), 42 | end: parseInt(messages[0].loc.end.offset, 10), 43 | arity: match[1] 44 | } : null; 45 | } 46 | 47 | return function (options) { 48 | const errorFile = options.errors; 49 | 50 | if (!errorFile) { 51 | console.log("no error file specified"); 52 | return { getErrors: file => [], hasErrors: file => false }; 53 | } 54 | 55 | if (lastErrorFileLoad && lastErrorFileLoad.errorFile == errorFile) { 56 | return lastErrorFileLoad.accessors; 57 | } 58 | 59 | const arityErrors = new Map(); 60 | 61 | const getErrors = file => arityErrors.get(file) || []; 62 | const hasErrors = file => arityErrors.has(file); 63 | 64 | function addArityError(info) { 65 | const file = getFileName(info.path); 66 | const errors = getErrors(file).concat([info]); 67 | arityErrors.set(file, errors); 68 | } 69 | 70 | let loaded = 0; 71 | try { 72 | const buffer = require('fs').readFileSync(errorFile, 'utf8'); 73 | const flowErrors = JSON.parse(buffer); 74 | for (const error of flowErrors.errors) { 75 | const info = matchArityError(error.message); 76 | if (info) { 77 | loaded++; 78 | addArityError(info); 79 | } 80 | } 81 | console.log(`worker: loaded ${ loaded } arity errors (of ${ flowErrors.errors.length } total) from ${ errorFile }`); 82 | } catch (err) { 83 | console.log(`worker: exception [${ err }] while loading '${ errorFile }', ${ loaded } errors loaded`); 84 | } 85 | 86 | const accessors = { getErrors, hasErrors }; 87 | lastErrorFileLoad = { errorFile, accessors }; 88 | return accessors; 89 | }; 90 | }(); 91 | 92 | // transform 93 | // 94 | const transform = function (file, api, options) { 95 | 96 | const { jscodeshift: j, stats } = api; 97 | const fileName = getFileName(file.path); 98 | 99 | const { getErrors, hasErrors } = loadArityErrors(options); 100 | 101 | // extract a name from an id/qid node. qualifiers are dotted 102 | function getName(id) { 103 | switch (id.type) { 104 | case 'Identifier': 105 | return id.name; 106 | case 'QualifiedTypeIdentifier': 107 | return `${ id.qualification.name }.${ id.id.name }`; 108 | default: 109 | return null; 110 | } 111 | } 112 | 113 | // process an id or qualified id expr from a type annotation. 114 | // we add explicit `any` type arguments if: 115 | // 1. no type args are already specified 116 | // 2. the expr is the source of an arity error loaded with `--errors` 117 | // 118 | function process(annoPath) { 119 | const { id, typeParameters, start, end } = annoPath.value; 120 | if (typeParameters) return false; 121 | 122 | const name = getName(id); 123 | if (!name) return false; 124 | 125 | for (const info of getErrors(fileName)) { 126 | // NOTE: many files may share the same base name, and we want to avoid 127 | // path checks to keep use of this mod from getting too fussy (consider 128 | // root stripping, includes, etc.). So we go ahead and do the mod if we 129 | // get a base name + location match, on the premise that false positives 130 | // will be very unlikely unless there are multiple copies of an identical 131 | // file, in which case we'll effectively be using the first error record 132 | // as a proxy for all subsequent ones, which is fine. 133 | 134 | if (start == info.start && end == info.end) { 135 | // build arglist and attach 136 | const params = []; 137 | for (let i = 0; i < info.arity; i++) { 138 | params.push(j.anyTypeAnnotation()); 139 | } 140 | const withParams = j.genericTypeAnnotation(id, j.typeParameterInstantiation(params)); 141 | j(annoPath).replaceWith(withParams); 142 | 143 | return true; 144 | } 145 | } 146 | 147 | return false; 148 | } 149 | 150 | // true if annotation expr is *not* part of a typeof expression. 151 | // we need this to bypass conversions in `typeof Foo` exprs 152 | // (there `Foo` is a value expr, but it's parsed as an annotation) 153 | function notTypeOf(annoPath) { 154 | let path = annoPath; 155 | while (path = path.parent) { 156 | if (path.value && path.value.type == 'TypeofTypeAnnotation') { 157 | return false; 158 | } 159 | } 160 | return true; 161 | } 162 | 163 | // main 164 | // 165 | if (file.source.indexOf('@generated') == -1 && (fileName.endsWith('.js.flow') || file.source.indexOf('@flow') >= 0) && hasErrors(fileName)) { 166 | let processed = 0; 167 | let root = j(file.source); 168 | root.find(j.GenericTypeAnnotation).filter(notTypeOf).forEach(anno => { 169 | if (process(anno)) processed++; 170 | }); 171 | return processed > 0 ? root.toSource() : null; 172 | } else { 173 | return null; 174 | } 175 | }; 176 | 177 | module.exports = transform; -------------------------------------------------------------------------------- /transforms/strict-type-args/README.md: -------------------------------------------------------------------------------- 1 | ### `strict-type-args` 2 | 3 | #### What this codemod is for 4 | 5 | Running this codemod will convert implicit polymorphic type applications into explicit ones - this prepares code for Flow's upcoming switch to strict type argument processing. 6 | 7 | ###### Sidebar: 8 | * “Polymorphic type application” describes a type expression like `Promise`, which applies the type argument string to the polymorphic type `Promise`. 9 | * "in annotations" is important because this rule change does not include value expressions, such as references to polymorphic classes in runtime code, or calls to polymorphic functions. 10 | 11 | Up to now, Flow would let you use polymorphic types without arguments (e.g., simply `Promise`) and silently fill in the missing arguments with `any` to make a type application. While this behavior made certain annotations a bit more concise, it also lead to lots of confusion: among other things, 12 | * `any` isn’t a self-evident choice of default - for example, it’s easy to assume that argument types are being inferred instead 13 | * `any` can suppress errors in surprising ways, and being invisible makes its effects even harder to spot 14 | * a lack of type arguments can make it non-obvious that a type is polymorphic at all. 15 | 16 | To illustrate the problem, here’s some code that Flow currently signs off on: 17 | ``` 18 | var set: Set = new Set(); // set's type is actually Set, so 19 | set.add('x'); // even though this happens, 20 | for (const n: number of set.values()) { // this produces no error. 21 | // land of broken promises 22 | } 23 | ``` 24 | With the new rule, here’s the error you’d get for this code: 25 | ``` 26 | var set: Set = new Set(); 27 | ^^^ Set. Application of polymorphic type needs . 28 | ``` 29 | 30 | This codemod prepares existing code for this rule change by manifesting the implicit `any` type arguments explicitly, based on errors generated from Flow. For example, 31 | 32 | ``` 33 | let map: Map = ... 34 | ``` 35 | 36 | ...becomes 37 | 38 | ``` 39 | let map: Map = ... 40 | ``` 41 | 42 | `any` is used for all inserted type arguments, duplicating the implicit previous behavior exactly. However, these `any` arguments can often be replaced with something better: see **Post-codemod options**, below. 43 | 44 | #### Running the codemod 45 | 46 | * Generate Flow errors: 47 | 48 | This transform is driven by a Flow error set. 49 | 50 | To generate the necessary errors, you must be running Flow version 0.25 51 | or above. Currently the error is disabled by default; enable it by adding 52 | the following line to your `.flowconfig`: 53 | 54 | ``` 55 | [options] 56 | experimental.strict_type_args=true 57 | ``` 58 | 59 | (This error will be enabled by default in an upcoming version.) 60 | 61 | With this config in place, run the following command to generate a JSON 62 | error file for the transform to use: 63 | 64 | ``` 65 | flow --show-all-errors --json > 66 | ``` 67 | 68 | * Run the transform: 69 | 70 | Run the transform with the following `jscodeshift` command line: 71 | 72 | ``` 73 | jscodeshift 74 | -t flow-codemod/transforms/strict-type-args/strict-type-args.js 75 | 76 | --errors= 77 | --extensions=js,flow 78 | ``` 79 | 80 | ###### KNOWN ISSUES 81 | 82 | 1. jscodeshift currently uses Babel 5, which fails to parse certain JS idioms. 83 | Files that fail to parse will not be transformed, unfortunately. 84 | 85 | 2. The Flow and Babel 5 parsers sometimes disagree on the position of expressions 86 | due to tab expansion. If certain type annotations fail to convert even when the 87 | files are successfully parsed - in particular, if other annotations within the 88 | same file convert successfully - try [converting tabs to spaces](http://i.imgur.com/qx2VUgo.gif) and rerunning the 89 | transform. 90 | 91 | #### Post-codemod options 92 | 93 | The goal of this codemod is to avoid new errors in existing code, but leave all other type checking completely unchanged. So it's very conservative, simply replacing the invisible `any` type arguments with visible ones. 94 | 95 | Of course, in many cases Flow will do more and better checking if you use something besides `any`. The rest of this note is a quick guide to your choices in common situations. 96 | 97 | ##### Use a concrete type 98 | 99 | In the example we started with, `set`’s type is obviously `Set`, and saying so is all upside. This is the thing to do if the “meaning” of the type parameter(s) is clear (in the sense of, “Set’s type parameter is the type of elements in the set”), and you can express the argument type(s) conveniently. 100 | 101 | ##### Use `*` 102 | 103 | In cases where it’s either unclear what the type argument “should” be, or inconvenient to express it, a second option is to use `*`. This tells Flow to use whatever type has been inferred from context - for example, using `Set<*>` above will give you exactly the same type errors as `Set`. 104 | 105 | ##### Use `any` 106 | 107 | There are some situations where specifying any as a type argument is really your best option. These are broadly the same kinds of situations where any is a reasonable choice at the top level: where things are too variable, or dynamic, to express a strict type practically. 108 | 109 | ##### React 110 | 111 | For React code, it's often convenient (and not harmful to client code safety) to leave the any arguments in place. 112 | 113 | A pervasive example is the return type of `render()`: post-codemod code will look like this: 114 | ``` 115 | render(): React.Element { ... } 116 | ``` 117 | `React.Element`'s type parameter describes the element's properties. But there's typically no safety benefit to describing this with a concrete type, because client code doesn't normally use the value returned by `render()`. 118 | Also, since a Component's properties are decoupled from the properties of the Element(s) produced by render, it isn't simply a matter of sharing a common properties type between the two. 119 | 120 | ###### Note on React.Element<*>: 121 | 122 | If you have an itch to try this (despite its arguable safety benefit per above), bear in mind that while simple cases (for instance, cases where render only returns a single kind of Element, as opposed to conditionally returning one of several choices), will work fine, more variable render methods will sometimes produce Elements with mutually incompatible property shapes, which will cause errors whose root causes can be hard to trace. (The technical tl;dr is basically `React.Element | React.Element != React.Element`.) 123 | -------------------------------------------------------------------------------- /transforms/strict-type-args/src/strict-type-args.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * Add explicit arguments to polymorphic type application expressions, 8 | * based on errors from Flow >= 0.25. 9 | * 10 | * See ../README.md for instructions. 11 | * 12 | * @flow 13 | */ 14 | 15 | 'use-strict'; 16 | 17 | // minimal API type info in lieu of full libdef 18 | // 19 | interface Collection { 20 | find(type: Object, filter?: (node: Object) => boolean): Collection; 21 | filter(callback: (node: Object) => boolean): Collection; 22 | forEach(callback: (node: Object) => void): Collection; 23 | replaceWith: (node: Object) => Collection; 24 | toSource(options?: Object): string; 25 | } 26 | 27 | type JSCodeShift = { 28 | (source: Object | Array | string): Collection, 29 | // node types 30 | GenericTypeAnnotation: Object, 31 | // builders 32 | genericTypeAnnotation: () => Object, 33 | typeParameterInstantiation: (args: Array) => Object, 34 | anyTypeAnnotation: () => Object, 35 | } 36 | 37 | type Options = { [key: string]: string }; 38 | 39 | type Transform = ( 40 | fileInfo: { path: string, source: string }, 41 | api: { jscodeshift: JSCodeShift, stats: (stat: string) => void }, 42 | options: Options, 43 | ) => ?string; 44 | 45 | // loaded error info 46 | type Info = { path: string, start: number, end: number, arity: number }; 47 | 48 | // accessors for loaded errors 49 | type ErrorAccessors = { 50 | getErrors: (file: string) => Array, 51 | hasErrors: (file: string) => boolean, 52 | }; 53 | 54 | // extract base filename from given path 55 | const getFileName = path => path.replace(/^.*[\\\/]/, '') 56 | 57 | // load arity errors from given file (once per worker), return accessors 58 | // 59 | const loadArityErrors: (options: Options) => ErrorAccessors = function() { 60 | 61 | let lastErrorFileLoad: ?{ 62 | errorFile: string, 63 | accessors: ErrorAccessors, 64 | } = null; 65 | 66 | const re = /Application of polymorphic type needs 1 && messages[1].descr.match(re); 70 | return match ? { 71 | path: messages[0].path, 72 | start: parseInt(messages[0].loc.start.offset, 10), 73 | end: parseInt(messages[0].loc.end.offset, 10), 74 | arity: match[1], 75 | } : null; 76 | } 77 | 78 | return function(options) { 79 | const errorFile = options.errors; 80 | 81 | if (!errorFile) { 82 | console.log("no error file specified"); 83 | return { getErrors: (file) => [], hasErrors: (file) => false }; 84 | } 85 | 86 | if (lastErrorFileLoad && lastErrorFileLoad.errorFile == errorFile) { 87 | return lastErrorFileLoad.accessors; 88 | } 89 | 90 | const arityErrors: Map> = new Map(); 91 | 92 | const getErrors = (file) => arityErrors.get(file) || []; 93 | const hasErrors = (file) => arityErrors.has(file); 94 | 95 | function addArityError(info) { 96 | const file = getFileName(info.path); 97 | const errors = getErrors(file).concat([info]); 98 | arityErrors.set(file, errors); 99 | } 100 | 101 | let loaded = 0; 102 | try { 103 | const buffer = require('fs').readFileSync(errorFile, 'utf8'); 104 | const flowErrors = JSON.parse(buffer); 105 | for (const error of flowErrors.errors) { 106 | const info = matchArityError(error.message); 107 | if (info) { 108 | loaded++; 109 | addArityError(info); 110 | } 111 | } 112 | console.log(`worker: loaded ${loaded} arity errors (of ${flowErrors.errors.length} total) from ${errorFile}`); 113 | } catch (err) { 114 | console.log(`worker: exception [${err}] while loading '${errorFile}', ${loaded} errors loaded`); 115 | } 116 | 117 | const accessors = { getErrors, hasErrors }; 118 | lastErrorFileLoad = { errorFile, accessors }; 119 | return accessors; 120 | } 121 | }(); 122 | 123 | // transform 124 | // 125 | const transform: Transform = function(file, api, options) { 126 | 127 | const { jscodeshift: j, stats } = api; 128 | const fileName = getFileName(file.path); 129 | 130 | const { getErrors, hasErrors } = loadArityErrors(options); 131 | 132 | // extract a name from an id/qid node. qualifiers are dotted 133 | function getName(id) { 134 | switch (id.type) { 135 | case 'Identifier': 136 | return id.name; 137 | case 'QualifiedTypeIdentifier': 138 | return `${id.qualification.name}.${id.id.name}`; 139 | default: 140 | return null; 141 | } 142 | } 143 | 144 | // process an id or qualified id expr from a type annotation. 145 | // we add explicit `any` type arguments if: 146 | // 1. no type args are already specified 147 | // 2. the expr is the source of an arity error loaded with `--errors` 148 | // 149 | function process(annoPath):boolean { 150 | const { id, typeParameters, start, end } = annoPath.value; 151 | if (typeParameters) return false; 152 | 153 | const name: ?string = getName(id); 154 | if (!name) return false; 155 | 156 | for (const info of getErrors(fileName)) { 157 | // NOTE: many files may share the same base name, and we want to avoid 158 | // path checks to keep use of this mod from getting too fussy (consider 159 | // root stripping, includes, etc.). So we go ahead and do the mod if we 160 | // get a base name + location match, on the premise that false positives 161 | // will be very unlikely unless there are multiple copies of an identical 162 | // file, in which case we'll effectively be using the first error record 163 | // as a proxy for all subsequent ones, which is fine. 164 | 165 | if (start == info.start && end == info.end) { 166 | // build arglist and attach 167 | const params = []; 168 | for (let i = 0; i < info.arity; i++) { 169 | params.push(j.anyTypeAnnotation()); 170 | } 171 | const withParams = j.genericTypeAnnotation(id, 172 | j.typeParameterInstantiation(params) 173 | ); 174 | j(annoPath).replaceWith(withParams); 175 | 176 | return true; 177 | } 178 | } 179 | 180 | return false; 181 | } 182 | 183 | // true if annotation expr is *not* part of a typeof expression. 184 | // we need this to bypass conversions in `typeof Foo` exprs 185 | // (there `Foo` is a value expr, but it's parsed as an annotation) 186 | function notTypeOf(annoPath):boolean { 187 | let path = annoPath; 188 | while (path = path.parent) { 189 | if (path.value && path.value.type == 'TypeofTypeAnnotation') { 190 | return false; 191 | } 192 | } 193 | return true; 194 | } 195 | 196 | // main 197 | // 198 | if (file.source.indexOf('@generated') == -1 && 199 | (fileName.endsWith('.js.flow') || file.source.indexOf('@flow') >= 0) 200 | && hasErrors(fileName)) { 201 | let processed = 0; 202 | let root = j(file.source); 203 | root.find(j.GenericTypeAnnotation). 204 | filter(notTypeOf). 205 | forEach((anno) => { 206 | if (process(anno)) 207 | processed++; 208 | } 209 | ); 210 | return processed > 0 ? root.toSource() : null; 211 | } else { 212 | return null; 213 | } 214 | } 215 | 216 | module.exports = transform; 217 | --------------------------------------------------------------------------------