├── .README ├── README.md └── rules │ ├── array-style-complex-type.md │ ├── array-style-simple-type.md │ ├── arrow-parens.md │ ├── boolean-style.md │ ├── define-flow-type.md │ ├── delimiter-dangle.md │ ├── enforce-line-break.md │ ├── generic-spacing.md │ ├── interface-id-match.md │ ├── newline-after-flow-annotation.md │ ├── no-dupe-keys.md │ ├── no-duplicate-type-union-intersection-members.md │ ├── no-existential-type.md │ ├── no-flow-fix-me-comments.md │ ├── no-internal-flow-type.md │ ├── no-mixed.md │ ├── no-mutable-array.md │ ├── no-primitive-constructor-types.md │ ├── no-types-missing-file-annotation.md │ ├── no-unused-expressions.md │ ├── no-weak-types.md │ ├── object-type-curly-spacing.md │ ├── object-type-delimiter.md │ ├── quotes.md │ ├── require-compound-type-alias.md │ ├── require-exact-type.md │ ├── require-indexer-name.md │ ├── require-inexact-type.md │ ├── require-parameter-type.md │ ├── require-readonly-react-props.md │ ├── require-return-type.md │ ├── require-types-at-top.md │ ├── require-valid-file-annotation.md │ ├── require-variable-type.md │ ├── semi.md │ ├── sort-keys.md │ ├── sort-type-union-intersection-members.md │ ├── space-after-type-colon.md │ ├── space-before-generic-bracket.md │ ├── space-before-type-colon.md │ ├── spread-exact-type.md │ ├── type-id-match.md │ ├── type-import-style.md │ ├── union-intersection-spacing.md │ ├── use-flow-type.md │ ├── use-read-only-spread.md │ └── valid-syntax.md ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── bin │ ├── addAssertions.js │ ├── checkDocs.js │ ├── checkTests.js │ └── utilities.js ├── configs │ └── recommended.json ├── index.js ├── rules │ ├── arrayStyle │ │ ├── index.js │ │ ├── isSimpleType.js │ │ └── needWrap.js │ ├── arrayStyleComplexType.js │ ├── arrayStyleSimpleType.js │ ├── arrowParens.js │ ├── booleanStyle.js │ ├── defineFlowType.js │ ├── delimiterDangle.js │ ├── enforceLineBreak.js │ ├── genericSpacing.js │ ├── interfaceIdMatch.js │ ├── newlineAfterFlowAnnotation.js │ ├── noDupeKeys.js │ ├── noDuplicateTypeUnionIntersectionMembers.js │ ├── noExistentialType.js │ ├── noFlowFixMeComments.js │ ├── noInternalFlowType.js │ ├── noMixed.js │ ├── noMutableArray.js │ ├── noPrimitiveConstructorTypes.js │ ├── noTypesMissingFileAnnotation.js │ ├── noUnusedExpressions.js │ ├── noWeakTypes.js │ ├── objectTypeCurlySpacing.js │ ├── objectTypeDelimiter.js │ ├── quotes.js │ ├── requireCompoundTypeAlias.js │ ├── requireExactType.js │ ├── requireIndexerName.js │ ├── requireInexactType.js │ ├── requireParameterType.js │ ├── requireReadonlyReactProps.js │ ├── requireReturnType.js │ ├── requireTypesAtTop.js │ ├── requireValidFileAnnotation.js │ ├── requireVariableType.js │ ├── semi.js │ ├── sortKeys.js │ ├── sortTypeUnionIntersectionMembers.js │ ├── spaceAfterTypeColon.js │ ├── spaceBeforeGenericBracket.js │ ├── spaceBeforeTypeColon.js │ ├── spreadExactType.js │ ├── typeColonSpacing │ │ ├── evaluateFunctions.js │ │ ├── evaluateObjectTypeIndexer.js │ │ ├── evaluateObjectTypeProperty.js │ │ ├── evaluateReturnType.js │ │ ├── evaluateTypeCastExpression.js │ │ ├── evaluateTypical.js │ │ ├── evaluateVariables.js │ │ ├── index.js │ │ └── reporter.js │ ├── typeIdMatch.js │ ├── typeImportStyle.js │ ├── unionIntersectionSpacing.js │ ├── useFlowType.js │ ├── useReadOnlySpread.js │ └── validSyntax.js └── utilities │ ├── checkFlowFileAnnotation.js │ ├── fuzzyStringMatch.js │ ├── getBuiltinRule.js │ ├── getParameterName.js │ ├── getTokenAfterParens.js │ ├── getTokenBeforeParens.js │ ├── index.js │ ├── isFlowFile.js │ ├── isFlowFileAnnotation.js │ ├── isNoFlowFile.js │ ├── isNoFlowFileAnnotation.js │ ├── iterateFunctionNodes.js │ ├── quoteName.js │ └── spacingFixers.js └── tests └── rules ├── assertions ├── arrayStyleComplexType.js ├── arrayStyleSimpleType.js ├── arrowParens.js ├── booleanStyle.js ├── defineFlowType.js ├── delimiterDangle.js ├── enforceLineBreak.js ├── genericSpacing.js ├── interfaceIdMatch.js ├── newlineAfterFlowAnnotation.js ├── noDupeKeys.js ├── noDuplicateTypeUnionIntersectionMembers.js ├── noExistentialType.js ├── noFlowFixMeComments.js ├── noInternalFlowType.js ├── noMixed.js ├── noMutableArray.js ├── noPrimitiveConstructorTypes.js ├── noTypesMissingFileAnnotation.js ├── noUnusedExpressions.js ├── noWeakTypes.js ├── objectTypeCurlySpacing.js ├── objectTypeDelimiter.js ├── quotes.js ├── requireCompoundTypeAlias.js ├── requireExactType.js ├── requireIndexerName.js ├── requireInexactType.js ├── requireParameterType.js ├── requireReadonlyReactProps.js ├── requireReturnType.js ├── requireTypesAtTop.js ├── requireValidFileAnnotation.js ├── requireVariableType.js ├── semi.js ├── sortKeys.js ├── sortTypeUnionIntersectionMembers.js ├── spaceAfterTypeColon.js ├── spaceBeforeGenericBracket.js ├── spaceBeforeTypeColon.js ├── spreadExactType.js ├── typeIdMatch.js ├── typeImportStyle.js ├── unionIntersectionSpacing.js ├── useFlowType.js ├── useReadOnlySpread.js └── validSyntax.js └── index.js /.README/rules/array-style-complex-type.md: -------------------------------------------------------------------------------- 1 | ### `array-style-complex-type` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces a particular annotation style of complex types. 6 | 7 | Type is considered complex in these cases: 8 | 9 | * [Maybe type](https://flow.org/en/docs/types/maybe/) 10 | * [Function type](https://flow.org/en/docs/types/functions/) 11 | * [Object type](https://flow.org/en/docs/types/objects/) 12 | * [Tuple type](https://flow.org/en/docs/types/tuples/) 13 | * [Union type](https://flow.org/en/docs/types/unions/) 14 | * [Intersection type](https://flow.org/en/docs/types/intersections/) 15 | 16 | This rule takes one argument. 17 | 18 | If it is `'verbose'` then a problem is raised when using `Type[]` instead of `Array`. 19 | 20 | If it is `'shorthand'` then a problem is raised when using `Array` instead of `Type[]`. 21 | 22 | The default value is `'verbose'`. 23 | 24 | 25 | -------------------------------------------------------------------------------- /.README/rules/array-style-simple-type.md: -------------------------------------------------------------------------------- 1 | ### `array-style-simple-type` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces a particular array type annotation style of simple types. 6 | 7 | Type is considered simple in these cases: 8 | 9 | * [Primitive types](https://flow.org/en/docs/types/primitives/) 10 | * [Literal types](https://flow.org/en/docs/types/literals/) 11 | * [Mixed type](https://flow.org/en/docs/types/mixed/) 12 | * [Any type](https://flow.org/en/docs/types/any/) 13 | * [Class type](https://flow.org/en/docs/types/classes/) 14 | * [Generic type](https://flow.org/en/docs/types/generics/) 15 | * Array type [shorthand notation](https://flow.org/en/docs/types/arrays/#toc-array-type-shorthand-syntax) 16 | 17 | This rule takes one argument. 18 | 19 | If it is `'verbose'` then a problem is raised when using `Type[]` instead of `Array`. 20 | 21 | If it is `'shorthand'` then a problem is raised when using `Array` instead of `Type[]`. 22 | 23 | The default value is `'verbose'`. 24 | 25 | 26 | -------------------------------------------------------------------------------- /.README/rules/arrow-parens.md: -------------------------------------------------------------------------------- 1 | ### `arrow-parens` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces the consistent use of parentheses in arrow functions. 6 | 7 | This rule has a string option and an object one. 8 | 9 | String options are: 10 | 11 | - `"always"` (default) requires parens around arguments in all cases. 12 | - `"as-needed"` enforces no braces where they can be omitted. 13 | 14 | Object properties for variants of the `"as-needed"` option: 15 | 16 | - `"requireForBlockBody": true` modifies the as-needed rule in order to require parens if the function body is in an instructions block (surrounded by braces). 17 | 18 | 19 | -------------------------------------------------------------------------------- /.README/rules/boolean-style.md: -------------------------------------------------------------------------------- 1 | ### `boolean-style` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces a particular style for boolean type annotations. This rule takes one argument. 6 | 7 | If it is `'boolean'` then a problem is raised when using `bool` instead of `boolean`. 8 | 9 | If it is `'bool'` then a problem is raised when using `boolean` instead of `bool`. 10 | 11 | The default value is `'boolean'`. 12 | 13 | 14 | -------------------------------------------------------------------------------- /.README/rules/define-flow-type.md: -------------------------------------------------------------------------------- 1 | ### `define-flow-type` 2 | 3 | Marks Flow type identifiers as defined. 4 | 5 | Used to suppress [`no-undef`](http://eslint.org/docs/rules/no-undef) reporting of type identifiers. 6 | 7 | 8 | -------------------------------------------------------------------------------- /.README/rules/delimiter-dangle.md: -------------------------------------------------------------------------------- 1 | ### `delimiter-dangle` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent use of trailing commas in Object and Tuple annotations. 6 | 7 | This rule takes three arguments where the possible values are the same as ESLint's default `comma-dangle` rule: 8 | 9 | 1. The first argument is for Object and Tuple annotations. The default value is `'never'`. 10 | 2. The second argument is used for Interface annotations. This defaults to the value of the first argument. 11 | 3. The third argument is used for inexact object notation (trailing `...`). The default value is `'never'`. 12 | 13 | If it is `'never'` then a problem is raised when there is a trailing comma. 14 | 15 | If it is `'always'` then a problem is raised when there is no trailing comma. 16 | 17 | If it is `'always-multiline'` then a problem is raised when there is no trailing comma on a multi-line definition, or there _is_ a trailing comma on a single-line definition. 18 | 19 | If it is `'only-multiline'` then a problem is raised when there is a trailing comma on a single-line definition. It allows, but does not enforce, trailing commas on multi-line definitions. 20 | 21 | 22 | -------------------------------------------------------------------------------- /.README/rules/enforce-line-break.md: -------------------------------------------------------------------------------- 1 | ### `enforce-line-break` 2 | 3 | This rule enforces line breaks between type definitions. 4 | 5 | 6 | -------------------------------------------------------------------------------- /.README/rules/generic-spacing.md: -------------------------------------------------------------------------------- 1 | ### `generic-spacing` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent spacing within generic type annotation parameters. 6 | 7 | This rule takes one argument. If it is `'never'` then a problem is raised when there is a space surrounding the generic type parameters. If it is `'always'` then a problem is raised when there is no space surrounding the generic type parameters. 8 | 9 | The default value is `'never'`. 10 | 11 | 12 | -------------------------------------------------------------------------------- /.README/rules/interface-id-match.md: -------------------------------------------------------------------------------- 1 | ### `interface-id-match` 2 | 3 | Enforces a consistent naming pattern for interfaces. 4 | 5 | #### Options 6 | 7 | This rule requires a text RegExp: 8 | 9 | ```js 10 | { 11 | "rules": { 12 | "flowtype/interface-id-match": [ 13 | 2, 14 | "^([A-Z][a-z0-9]*)+Type$" 15 | ] 16 | } 17 | } 18 | ``` 19 | 20 | `'^([A-Z][a-z0-9]*)+Type$$'` is the default pattern. 21 | 22 | 23 | -------------------------------------------------------------------------------- /.README/rules/newline-after-flow-annotation.md: -------------------------------------------------------------------------------- 1 | ### `newline-after-flow-annotation` 2 | 3 | This rule requires an empty line after the Flow annotation. 4 | 5 | #### Options 6 | 7 | The rule has a string option: 8 | 9 | * `"always"` (default): Enforces that `@flow` annotations be followed by an empty line, separated by newline (LF) 10 | * `"always-windows"`: Identical to "always", but will use a CRLF when autofixing 11 | * `"never"`: Enforces that `@flow` annotations are not followed by empty lines 12 | 13 | ```js 14 | { 15 | "rules": { 16 | "flowtype/newline-after-flow-annotation": [ 17 | 2, 18 | "always" 19 | ] 20 | } 21 | } 22 | ``` 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.README/rules/no-dupe-keys.md: -------------------------------------------------------------------------------- 1 | ### `no-dupe-keys` 2 | 3 | Checks for duplicate properties in Object annotations. 4 | 5 | This rule mirrors ESLint's [no-dupe-keys](http://eslint.org/docs/rules/no-dupe-keys) rule. 6 | 7 | ```js 8 | { 9 | "rules": { 10 | "flowtype/no-dupe-keys": 2 11 | } 12 | } 13 | ``` 14 | 15 | 16 | -------------------------------------------------------------------------------- /.README/rules/no-duplicate-type-union-intersection-members.md: -------------------------------------------------------------------------------- 1 | ### `no-duplicate-type-union-intersection-members` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Checks for duplicate members of a type union/intersection. 6 | 7 | #### Options 8 | 9 | You can disable checking intersection types using `checkIntersections`. 10 | 11 | * `true` (default) - check for duplicate members of intersection members. 12 | * `false` - do not check for duplicate members of intersection members. 13 | 14 | ```js 15 | { 16 | "rules": { 17 | "flowtype/no-duplicate-type-union-intersection-members": [ 18 | 2, 19 | { 20 | "checkIntersections": true 21 | } 22 | ] 23 | } 24 | } 25 | ``` 26 | 27 | You can disable checking union types using `checkUnions`. 28 | 29 | * `true` (default) - check for duplicate members of union members. 30 | * `false` - do not check for duplicate members of union members. 31 | 32 | ```js 33 | { 34 | "rules": { 35 | "flowtype/no-duplicate-type-union-intersection-members": [ 36 | 2, 37 | { 38 | "checkUnions": true 39 | } 40 | ] 41 | } 42 | } 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /.README/rules/no-existential-type.md: -------------------------------------------------------------------------------- 1 | ### `no-existential-type` 2 | 3 | Disallows use of the existential type (*). [See more](https://flow.org/en/docs/types/utilities/#toc-existential-type) 4 | 5 | ```js 6 | { 7 | "rules": { 8 | "flowtype/no-existential-type": 2 9 | } 10 | } 11 | ``` 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.README/rules/no-flow-fix-me-comments.md: -------------------------------------------------------------------------------- 1 | ### `no-flow-fix-me-comments` 2 | 3 | Disallows `$FlowFixMe` comment suppressions. 4 | 5 | This is especially useful as a warning to ensure instances of `$FlowFixMe` in your codebase get fixed over time. 6 | 7 | #### Options 8 | 9 | This rule takes an optional RegExp that comments a text RegExp that makes the supression valid. 10 | 11 | ```js 12 | { 13 | "rules": { 14 | "flowtype/no-flow-fix-me-comments": [ 15 | 1, 16 | "TODO\s+[0-9]+" 17 | ] 18 | } 19 | } 20 | ``` 21 | 22 | 23 | -------------------------------------------------------------------------------- /.README/rules/no-internal-flow-type.md: -------------------------------------------------------------------------------- 1 | ### `no-internal-flow-type` 2 | 3 | Warns against using internal Flow types such as `React$Node`, `React$Ref` and others and suggests using public alternatives instead (`React.Node`, `React.Ref`, …). 4 | 5 | 6 | -------------------------------------------------------------------------------- /.README/rules/no-mixed.md: -------------------------------------------------------------------------------- 1 | ### `no-mixed` 2 | 3 | Warns against "mixed" type annotations. 4 | These types are not strict enough and could often be made more specific. 5 | 6 | The following patterns are considered problems: 7 | 8 | 9 | -------------------------------------------------------------------------------- /.README/rules/no-mutable-array.md: -------------------------------------------------------------------------------- 1 | ### `no-mutable-array` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Requires use of [`$ReadOnlyArray`](https://github.com/facebook/flow/blob/v0.46.0/lib/core.js#L185) instead of just `Array` or array [shorthand notation](https://flow.org/en/docs/types/arrays/#toc-array-type-shorthand-syntax). `$ReadOnlyArray` is immutable array collection type and the superclass of Array and tuple types in Flow. Use of `$ReadOnlyArray` instead of `Array` can solve some "problems" in typing with Flow (e.g., [1](https://github.com/facebook/flow/issues/3425), [2](https://github.com/facebook/flow/issues/4251)). 6 | 7 | General reasons for using immutable data structures: 8 | 9 | * They are simpler to construct, test, and use 10 | * They help to avoid temporal coupling 11 | * Their usage is side-effect free (no defensive copies) 12 | * Identity mutability problem is avoided 13 | * They always have failure atomicity 14 | * They are much easier to cache 15 | 16 | Note that initialization of a variable with an empty array is considered valid (e.g., `const values: Array = [];`). This behavior resembles the behavior of Flow's [unsealed objects](https://flow.org/en/docs/types/objects/#toc-unsealed-objects), as it is assumed that empty array is intended to be mutated. 17 | 18 | 19 | -------------------------------------------------------------------------------- /.README/rules/no-primitive-constructor-types.md: -------------------------------------------------------------------------------- 1 | ### `no-primitive-constructor-types` 2 | 3 | Disallows use of primitive constructors as types, such as `Boolean`, `Number` and `String`. [See more](https://flowtype.org/docs/builtins.html). 4 | 5 | ```js 6 | { 7 | "rules": { 8 | "flowtype/no-primitive-constructor-types": 2 9 | } 10 | } 11 | ``` 12 | 13 | 14 | -------------------------------------------------------------------------------- /.README/rules/no-types-missing-file-annotation.md: -------------------------------------------------------------------------------- 1 | ### `no-types-missing-file-annotation` 2 | 3 | Disallows Flow type imports, aliases, and annotations in files missing a valid Flow file declaration (or a @noflow annotation). 4 | 5 | ```js 6 | { 7 | "rules": { 8 | "flowtype/no-types-missing-file-annotation": 2 9 | } 10 | } 11 | ``` 12 | 13 | 14 | -------------------------------------------------------------------------------- /.README/rules/no-unused-expressions.md: -------------------------------------------------------------------------------- 1 | ### `no-unused-expressions` 2 | 3 | An extension of [ESLint's `no-unused-expressions`](https://eslint.org/docs/rules/no-unused-expressions). 4 | This rule ignores type cast expressions and optional call expressions, but otherwise behaves the same as ESLint's 5 | `no-unused-expressions`. 6 | 7 | Bare type casts are useful, for example to assert the exhaustiveness of a `switch`: 8 | 9 | ```js 10 | type Action 11 | = { type: 'FOO', doFoo: (_: number) => void } 12 | | { type: 'BAR', doBar: (_: string) => void }; 13 | 14 | type State = { foo: number, bar: string }; 15 | 16 | function runFooBar(action: Action, state: State): void { 17 | switch (action.type) { 18 | case 'FOO': 19 | doFoo(state.foo); 20 | break; 21 | case 'BAR': 22 | doBar(state.bar); 23 | break; 24 | default: 25 | (action: empty); // type error when `Action` is extended with new types 26 | console.error(`Impossible action: ${action.toString()}`); 27 | } 28 | } 29 | ``` 30 | 31 | This rule takes the same arguments as ESLint's `no-unused-expressions`. See 32 | [that rule's documentation](https://eslint.org/docs/rules/no-unused-expressions) for details. 33 | 34 | 35 | -------------------------------------------------------------------------------- /.README/rules/no-weak-types.md: -------------------------------------------------------------------------------- 1 | ### `no-weak-types` 2 | 3 | Warns against weak type annotations *any*, *Object* and *Function*. 4 | These types can cause flow to silently skip over portions of your code, 5 | which would have otherwise caused type errors. 6 | 7 | This rule optionally takes one argument, an object to configure which type warnings to enable. By default, all of the 8 | warnings are enabled. e.g. to disable the `any` warning (allowing it to exist in your code), while continuing to warn 9 | about `Object` and `Function`: 10 | 11 | ```js 12 | { 13 | "rules": { 14 | "flowtype/no-weak-types": [2, { 15 | "any": false, 16 | "Object": true, 17 | "Function": true 18 | }] 19 | } 20 | } 21 | 22 | // or, the following is equivalent as default is true: 23 | 24 | { 25 | "rules": { 26 | "flowtype/no-weak-types": [2, { 27 | "any": false 28 | }] 29 | } 30 | } 31 | ``` 32 | 33 | 34 | -------------------------------------------------------------------------------- /.README/rules/object-type-curly-spacing.md: -------------------------------------------------------------------------------- 1 | ### `object-type-curly-spacing` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | This rule enforces consistent spacing inside braces of object types. 6 | 7 | #### Options 8 | 9 | The rule has a string option: 10 | 11 | * `"never"` (default): disallows spacing inside of braces. 12 | * `"always"`: requires spacing inside of braces. 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.README/rules/object-type-delimiter.md: -------------------------------------------------------------------------------- 1 | ### `object-type-delimiter` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent separators between properties in Flow object types. 6 | 7 | This rule takes one argument. 8 | 9 | If it is `'comma'` then a problem is raised when using `;` as a separator. 10 | 11 | If it is `'semicolon'` then a problem is raised when using `,` as a separator. 12 | 13 | The default value is `'comma'`. 14 | 15 | _This rule is ported from `babel/flow-object-type`, however the default option was changed._ 16 | 17 | 18 | -------------------------------------------------------------------------------- /.README/rules/quotes.md: -------------------------------------------------------------------------------- 1 | ### `quotes` 2 | 3 | Enforces single quotes or double quotes around string literals. 4 | 5 | #### Options 6 | 7 | The rule has string options of: 8 | 9 | * `"double"` (default) requires double quotes around string literals. 10 | * `"single"` requires single quotes around string literals. 11 | 12 | 13 | -------------------------------------------------------------------------------- /.README/rules/require-compound-type-alias.md: -------------------------------------------------------------------------------- 1 | ### `require-compound-type-alias` 2 | 3 | Requires to make a type alias for all [union](https://flow.org/en/docs/types/unions/) and [intersection](https://flow.org/en/docs/types/intersections/) types. If these are used in "raw" forms it might be tempting to just copy & paste them around the code. However, this brings sort of a source code pollution and unnecessary changes on several parts when these compound types need to be changed. 4 | 5 | #### Options 6 | 7 | The rule has two options: 8 | 9 | 1. a string option 10 | 11 | * `"always"` (default) 12 | * `"never"` 13 | 14 | 2. an object 15 | 16 | ```js 17 | { 18 | "rules": { 19 | "flowtype/require-compound-type-alias": [ 20 | 2, 21 | "always", 22 | { 23 | "allowNull": true 24 | } 25 | ] 26 | } 27 | } 28 | ``` 29 | 30 | * `allowNull` – allows compound types where one of the members is a `null`, e.g. `string | null`. 31 | 32 | 33 | -------------------------------------------------------------------------------- /.README/rules/require-exact-type.md: -------------------------------------------------------------------------------- 1 | ### `require-exact-type` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | This rule enforces [exact object types](https://flow.org/en/docs/types/objects/#toc-exact-object-types). 6 | 7 | #### Options 8 | 9 | The rule has one string option: 10 | 11 | * `"always"` (default): Report all object type definitions that aren't exact. 12 | * `"never"`: Report all object type definitions that are exact. 13 | 14 | ```js 15 | { 16 | "rules": { 17 | "flowtype/require-exact-type": [ 18 | 2, 19 | "always" 20 | ] 21 | } 22 | } 23 | 24 | { 25 | "rules": { 26 | "flowtype/require-exact-type": [ 27 | 2, 28 | "never" 29 | ] 30 | } 31 | } 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /.README/rules/require-indexer-name.md: -------------------------------------------------------------------------------- 1 | ### `require-indexer-name` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | This rule validates Flow object indexer name. 6 | 7 | #### Options 8 | 9 | The rule has a string option: 10 | 11 | * `"never"` (default): Never report files that are missing an indexer key name. 12 | * `"always"`: Always report files that are missing an indexer key name. 13 | 14 | ```js 15 | { 16 | "rules": { 17 | "flowtype/require-indexer-name": [ 18 | 2, 19 | "always" 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | 26 | -------------------------------------------------------------------------------- /.README/rules/require-inexact-type.md: -------------------------------------------------------------------------------- 1 | ### `require-inexact-type` 2 | 3 | This rule enforces explicit inexact object types. 4 | 5 | #### Options 6 | 7 | The rule has one string option: 8 | 9 | - `"always"` (default): Report all object type definitions that aren't explicit inexact, but ignore exact objects. 10 | - `"never"`: Report all object type definitions that are explicit inexact. 11 | 12 | ```js 13 | { 14 | "rules": { 15 | "flowtype/require-inexact-type": [ 16 | 2, 17 | "always" 18 | ] 19 | } 20 | } 21 | 22 | { 23 | "rules": { 24 | "flowtype/require-inexact-type": [ 25 | 2, 26 | "never" 27 | ] 28 | } 29 | } 30 | ``` 31 | 32 | 33 | -------------------------------------------------------------------------------- /.README/rules/require-parameter-type.md: -------------------------------------------------------------------------------- 1 | ### `require-parameter-type` 2 | 3 | Requires that all function parameters have type annotations. 4 | 5 | #### Options 6 | 7 | You can skip all arrow functions by providing the `excludeArrowFunctions` option with `true`. 8 | 9 | Alternatively, you can want to exclude only concise arrow functions (e.g. `x => x * 2`). Provide `excludeArrowFunctions` with `expressionsOnly` for this. 10 | 11 | ```js 12 | { 13 | "rules": { 14 | "flowtype/require-parameter-type": [ 15 | 2, 16 | { 17 | "excludeArrowFunctions": true 18 | } 19 | ] 20 | } 21 | } 22 | 23 | { 24 | "rules": { 25 | "flowtype/require-parameter-type": [ 26 | 2, 27 | { 28 | "excludeArrowFunctions": "expressionsOnly" 29 | } 30 | ] 31 | } 32 | } 33 | ``` 34 | 35 | You can exclude parameters that match a certain regex by using `excludeParameterMatch`. 36 | 37 | ```js 38 | { 39 | "rules": { 40 | "flowtype/require-parameter-type": [ 41 | 2, 42 | { 43 | "excludeParameterMatch": "^_" 44 | } 45 | ] 46 | } 47 | } 48 | ``` 49 | 50 | This excludes all parameters that start with an underscore (`_`). 51 | The default pattern is `a^`, which doesn't match anything, i.e., all parameters are checked. 52 | 53 | 54 | -------------------------------------------------------------------------------- /.README/rules/require-readonly-react-props.md: -------------------------------------------------------------------------------- 1 | ### `require-readonly-react-props` 2 | 3 | This rule validates that React props are marked as `$ReadOnly`. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as `$ReadOnly` avoids these issues. 4 | 5 | The rule tries its best to work with both class and functional components. For class components, it does a fuzzy check for one of "Component", "PureComponent", "React.Component" and "React.PureComponent". It doesn't actually infer that those identifiers resolve to a proper `React.Component` object. 6 | 7 | For example, this will NOT be checked: 8 | 9 | ```js 10 | import MyReact from 'react'; 11 | class Foo extends MyReact.Component { } 12 | ``` 13 | 14 | As a result, you can safely use other classes without getting warnings from this rule: 15 | 16 | ```js 17 | class MyClass extends MySuperClass { } 18 | ``` 19 | 20 | React's functional components are hard to detect statically. The way it's done here is by searching for JSX within a function. When present, a function is considered a React component: 21 | 22 | ```js 23 | // this gets checked 24 | type Props = { }; 25 | function MyComponent(props: Props) { 26 | return

; 27 | } 28 | 29 | // this doesn't get checked since no JSX is present in a function 30 | type Options = { }; 31 | function SomeHelper(options: Options) { 32 | // ... 33 | } 34 | 35 | // this doesn't get checked since no JSX is present directly in a function 36 | function helper() { return

} 37 | function MyComponent(props: Props) { 38 | return helper(); 39 | } 40 | ``` 41 | 42 | The rule only works for locally defined props that are marked with a `$ReadOnly` or using covariant notation. It doesn't work with imported props: 43 | 44 | ```js 45 | // the rule has no way of knowing whether ImportedProps are read-only 46 | import { type ImportedProps } from './somewhere'; 47 | class Foo extends React.Component { } 48 | 49 | 50 | // the rule also checks for covariant properties 51 | type Props = {| 52 | +foo: string 53 | |}; 54 | class Bar extends React.Component { } 55 | 56 | // this fails because the object is not fully read-only 57 | type Props = {| 58 | +foo: string, 59 | bar: number, 60 | |}; 61 | class Bar extends React.Component { } 62 | 63 | // this fails because spreading makes object mutable (as of Flow 0.98) 64 | // https://github.com/gajus/eslint-plugin-flowtype/pull/400#issuecomment-489813899 65 | type Props = {| 66 | +foo: string, 67 | ...bar, 68 | |}; 69 | class Bar extends React.Component { } 70 | ``` 71 | 72 | 73 | ```js 74 | { 75 | "rules": { 76 | "flowtype/require-readonly-react-props": 2 77 | } 78 | } 79 | ``` 80 | 81 | 82 | Optionally, you can enable support for [implicit exact Flow types](https://medium.com/flow-type/on-the-roadmap-exact-objects-by-default-16b72933c5cf) (useful when using `exact_by_default=true` Flow option): 83 | 84 | 85 | ```js 86 | { 87 | "rules": { 88 | "flowtype/require-readonly-react-props": [ 89 | 2, 90 | { 91 | "useImplicitExactTypes": true 92 | } 93 | ] 94 | } 95 | } 96 | ``` 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /.README/rules/require-return-type.md: -------------------------------------------------------------------------------- 1 | ### `require-return-type` 2 | 3 | Requires that functions have return type annotation. 4 | 5 | #### Options 6 | 7 | You can skip all arrow functions by providing the `excludeArrowFunctions` option with `true`. 8 | 9 | Alternatively, you can exclude a concise arrow function (e.g. `() => 2`). Provide `excludeArrowFunctions` with `expressionsOnly` for this. 10 | 11 | ```js 12 | { 13 | "rules": { 14 | "flowtype/require-return-type": [ 15 | 2, 16 | "always", 17 | { 18 | "excludeArrowFunctions": true 19 | } 20 | ] 21 | } 22 | } 23 | 24 | { 25 | "rules": { 26 | "flowtype/require-return-type": [ 27 | 2, 28 | "always", 29 | { 30 | "excludeArrowFunctions": "expressionsOnly" 31 | } 32 | ] 33 | } 34 | } 35 | ``` 36 | 37 | You can exclude or include specific tests with the `includeOnlyMatching` and `excludeMatching` rules. 38 | 39 | ```js 40 | { 41 | "rules": { 42 | "flowtype/require-return-type": [ 43 | 2, 44 | "always", 45 | { 46 | "includeOnlyMatching": [ 47 | "^F.*", 48 | "Ba(r|z)" 49 | ] 50 | } 51 | ] 52 | } 53 | } 54 | 55 | { 56 | "rules": { 57 | "flowtype/require-return-type": [ 58 | 2, 59 | "always", 60 | { 61 | "excludeMatching": [ 62 | "^F.*", 63 | "Ba(r|z)" 64 | ] 65 | } 66 | ] 67 | } 68 | } 69 | 70 | ``` 71 | 72 | Both rules take an array that can contain either strings or valid RegExp statements. 73 | 74 | 75 | -------------------------------------------------------------------------------- /.README/rules/require-types-at-top.md: -------------------------------------------------------------------------------- 1 | ### `require-types-at-top` 2 | 3 | Requires all type declarations to be at the top of the file, after any import declarations. 4 | 5 | #### Options 6 | 7 | The rule has a string option: 8 | 9 | * `"never"` 10 | * `"always"` 11 | 12 | The default value is `"always"`. 13 | 14 | 15 | -------------------------------------------------------------------------------- /.README/rules/require-valid-file-annotation.md: -------------------------------------------------------------------------------- 1 | ### `require-valid-file-annotation` 2 | 3 | This rule validates Flow file annotations. 4 | 5 | This rule can optionally report missing or missed placed annotations, common typos (e.g. `// @floww`), and enforce a consistent annotation style. 6 | 7 | #### Options 8 | 9 | The rule has a string option: 10 | 11 | * `"never"` (default): Never report files that are missing an `@flow` annotation. 12 | * `"always"`: Always report files that are missing an `@flow` annotation 13 | 14 | This rule has an object option: 15 | 16 | * `"annotationStyle"` - Enforce a consistent file annotation style. 17 | * `"none"` (default): Either annotation style is accepted. 18 | * `"line"`: Require single line annotations (i.e. `// @flow`). 19 | * `"block"`: Require block annotations (i.e. `/* @flow */`). 20 | 21 | * `"strict"` - Enforce a strict flow file annotation. 22 | * `false` (default): strict flow annotation is not required. 23 | * `true`: Require strict flow annotation (i.e. `// @flow strict`). 24 | 25 | ```js 26 | { 27 | "rules": { 28 | "flowtype/require-valid-file-annotation": [ 29 | 2, 30 | "always" 31 | ] 32 | } 33 | } 34 | 35 | { 36 | "rules": { 37 | "flowtype/require-valid-file-annotation": [ 38 | 2, 39 | "always", { 40 | "annotationStyle": "block", 41 | "strict": true, 42 | } 43 | ] 44 | } 45 | } 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /.README/rules/require-variable-type.md: -------------------------------------------------------------------------------- 1 | ### `require-variable-type` 2 | 3 | Requires that all variable declarators have type annotations. 4 | 5 | #### Options 6 | 7 | You can exclude variables that match a certain regex by using `excludeVariableMatch`. 8 | 9 | This excludes all parameters that start with an underscore (`_`). 10 | The default pattern is `a^`, which doesn't match anything, i.e., all parameters are checked. 11 | 12 | ```js 13 | { 14 | "rules": { 15 | "flowtype/require-variable-type": [ 16 | 2, 17 | { 18 | "excludeVariableMatch": "^_" 19 | } 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | 26 | You can choose specific variable types (`var`, `let`, and `const`) to ignore using `excludeVariableTypes`. 27 | 28 | This excludes `var` and `let` declarations from needing type annotations, but forces `const` declarations to have it. 29 | By default, all declarations are checked. 30 | 31 | ```js 32 | { 33 | "rules": { 34 | "flowtype/require-variable-type": [ 35 | 2, 36 | { 37 | "excludeVariableTypes": { 38 | "var": true, 39 | "let": true, 40 | "const": false, 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | ``` 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.README/rules/semi.md: -------------------------------------------------------------------------------- 1 | ### `semi` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent use of semicolons after type aliases. 6 | 7 | This rule takes one argument. If it is `'never'` then a problem is raised when there is a semicolon after a type alias. If it is `'always'` then a problem is raised when there is no semicolon after a type alias. 8 | 9 | The default value is `'always'`. 10 | 11 | 12 | -------------------------------------------------------------------------------- /.README/rules/sort-keys.md: -------------------------------------------------------------------------------- 1 | ### `sort-keys` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces natural, case-insensitive sorting of Object annotations. 6 | 7 | #### Options 8 | 9 | The first option specifies sort order. 10 | 11 | * `"asc"` (default) - enforce ascending sort order. 12 | * `"desc"` - enforce descending sort order. 13 | 14 | ```js 15 | { 16 | "rules": { 17 | "flowtype/sort-keys": [ 18 | 2, 19 | "asc" 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | 26 | -------------------------------------------------------------------------------- /.README/rules/sort-type-union-intersection-members.md: -------------------------------------------------------------------------------- 1 | ### `sort-type-union-intersection-members` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces that members of a type union/intersection are sorted alphabetically. 6 | 7 | #### Options 8 | 9 | You can specify the sort order using `order`. 10 | 11 | * `"asc"` (default) - enforce ascending sort order. 12 | * `"desc"` - enforce descending sort order. 13 | 14 | ```js 15 | { 16 | "rules": { 17 | "flowtype/sort-type-union-intersection-members": [ 18 | 2, 19 | { 20 | "order": "asc" 21 | } 22 | ] 23 | } 24 | } 25 | ``` 26 | 27 | You can disable checking intersection types using `checkIntersections`. 28 | 29 | * `true` (default) - enforce sort order of intersection members. 30 | * `false` - do not enforce sort order of intersection members. 31 | 32 | ```js 33 | { 34 | "rules": { 35 | "flowtype/sort-type-union-intersection-members": [ 36 | 2, 37 | { 38 | "checkIntersections": true 39 | } 40 | ] 41 | } 42 | } 43 | ``` 44 | 45 | You can disable checking union types using `checkUnions`. 46 | 47 | * `true` (default) - enforce sort order of union members. 48 | * `false` - do not enforce sort order of union members. 49 | 50 | ```js 51 | { 52 | "rules": { 53 | "flowtype/sort-type-union-intersection-members": [ 54 | 2, 55 | { 56 | "checkUnions": true 57 | } 58 | ] 59 | } 60 | } 61 | ``` 62 | 63 | You can specify the ordering of groups using `groupOrder`. 64 | 65 | Each member of the type is placed into a group, and then the rule sorts alphabetically within each group. 66 | The ordering of groups is determined by this option. 67 | 68 | * `keyword` - Keyword types (`any`, `string`, etc) 69 | * `named` - Named types (`A`, `A['prop']`, `B[]`, `Array`) 70 | * `literal` - Literal types (`1`, `'b'`, `true`, etc) 71 | * `function` - Function types (`() => void`) 72 | * `object` - Object types (`{ a: string }`, `{ [key: string]: number }`) 73 | * `tuple` - Tuple types (`[A, B, C]`) 74 | * `intersection` - Intersection types (`A & B`) 75 | * `union` - Union types (`A | B`) 76 | * `nullish` - `null` and `undefined` 77 | 78 | ```js 79 | { 80 | "rules": { 81 | "flowtype/sort-type-union-intersection-members": [ 82 | 2, 83 | { 84 | "groupOrder": [ 85 | 'keyword', 86 | 'named', 87 | 'literal', 88 | 'function', 89 | 'object', 90 | 'tuple', 91 | 'intersection', 92 | 'union', 93 | 'nullish', 94 | ] 95 | } 96 | ] 97 | } 98 | } 99 | ``` 100 | 101 | 102 | -------------------------------------------------------------------------------- /.README/rules/space-after-type-colon.md: -------------------------------------------------------------------------------- 1 | ### `space-after-type-colon` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent spacing after the type annotation colon. 6 | 7 | #### Options 8 | 9 | This rule has a string argument. 10 | 11 | * `"always"` (default): Require a space after the type annotation colon (e.g. foo: BarType). 12 | * `"never"`: Require no spaces after the type annotation colon (e.g. foo:BarType). 13 | 14 | This rule has an option object. 15 | 16 | * `"allowLineBreak"` - Allow a line break to count as a space following the annotation colon. 17 | * `"true"`: Enable 18 | * `"false"`: Disable 19 | 20 | { 21 | "rules": { 22 | "flowtype/space-after-type-colon": [ 23 | 2, 24 | "always", { 25 | "allowLineBreak": false 26 | } 27 | ] 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /.README/rules/space-before-generic-bracket.md: -------------------------------------------------------------------------------- 1 | ### `space-before-generic-bracket` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent spacing before the opening `<` of generic type annotation parameters. 6 | 7 | This rule takes one argument. If it is `'never'` then a problem is raised when there is a space before the `<`. If it is `'always'` then a problem is raised when there is no space before the `<`. 8 | 9 | The default value is `'never'`. 10 | 11 | 12 | -------------------------------------------------------------------------------- /.README/rules/space-before-type-colon.md: -------------------------------------------------------------------------------- 1 | ### `space-before-type-colon` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent spacing before the type annotation colon. 6 | 7 | This rule takes one argument. If it is `'always'` then a problem is raised when there is no space before the type annotation colon. If it is `'never'` then a problem is raised when there is a space before the type annotation colon. The default value is `'never'`. 8 | 9 | 10 | -------------------------------------------------------------------------------- /.README/rules/spread-exact-type.md: -------------------------------------------------------------------------------- 1 | ### `spread-exact-type` 2 | 3 | Enforce object types, that are spread to be exact type explicitly. 4 | 5 | 6 | -------------------------------------------------------------------------------- /.README/rules/type-id-match.md: -------------------------------------------------------------------------------- 1 | ### `type-id-match` 2 | 3 | Enforces a consistent naming pattern for type aliases. 4 | 5 | #### Options 6 | 7 | This rule requires a text RegExp: 8 | 9 | ```js 10 | { 11 | "rules": { 12 | "flowtype/type-id-match": [ 13 | 2, 14 | "^([A-Z][a-z0-9]*)+Type$" 15 | ] 16 | } 17 | } 18 | ``` 19 | 20 | `'^([A-Z][a-z0-9]*)+Type$$'` is the default pattern. 21 | 22 | 23 | -------------------------------------------------------------------------------- /.README/rules/type-import-style.md: -------------------------------------------------------------------------------- 1 | ### `type-import-style` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces a particular style for type imports: 6 | 7 | ``` 8 | // 'identifier' style 9 | import {type T, type U, type V} from '...'; 10 | 11 | // 'declaration' style 12 | import type {T, U, V} from '...'; 13 | ``` 14 | 15 | #### Options 16 | 17 | The rule has a string option: 18 | 19 | * `"identifier"` (default): Enforces that type imports are all in the 20 | 'identifier' style. 21 | * `"declaration"`: Enforces that type imports are all in the 'declaration' 22 | style. 23 | 24 | This rule has an object option: 25 | 26 | * `ignoreTypeDefault` - if `true`, when in "identifier" mode, default type imports will be ignored. Default is `false`. 27 | 28 | 29 | -------------------------------------------------------------------------------- /.README/rules/union-intersection-spacing.md: -------------------------------------------------------------------------------- 1 | ### `union-intersection-spacing` 2 | 3 | _The `--fix` option on the command line automatically fixes problems reported by this rule._ 4 | 5 | Enforces consistent spacing around union and intersection type separators (`|` and `&`). 6 | 7 | This rule takes one argument. If it is `'always'` then a problem is raised when there is no space around the separator. If it is `'never'` then a problem is raised when there is a space around the separator. 8 | 9 | The default value is `'always'`. 10 | 11 | 12 | -------------------------------------------------------------------------------- /.README/rules/use-flow-type.md: -------------------------------------------------------------------------------- 1 | ### `use-flow-type` 2 | 3 | Marks Flow [type alias](https://flowtype.org/docs/type-aliases.html) declarations as used. 4 | 5 | Used to suppress [`no-unused-vars`](http://eslint.org/docs/rules/no-unused-vars) errors that are triggered by type aliases. 6 | 7 | 8 | -------------------------------------------------------------------------------- /.README/rules/use-read-only-spread.md: -------------------------------------------------------------------------------- 1 | ### `use-read-only-spread` 2 | 3 | Warns against accidentally creating an object which is no longer read-only because of how spread operator works in Flow. Imagine the following code: 4 | 5 | ```flow js 6 | type INode = {| 7 | +type: string, 8 | |}; 9 | 10 | type Identifier = {| 11 | ...INode, 12 | +name: string, 13 | |}; 14 | ``` 15 | 16 | You might expect the identifier name to be read-only, however, that's not true ([flow.org/try](https://flow.org/try/#0C4TwDgpgBAkgcgewCbQLxQN4B8BQUoDUokAXFAM7ABOAlgHYDmANDlgL4DcOOx0MKdYDQBmNCFSjpseKADp58ZBBb4CdAIYBbCGUq1GLdlxwBjBHUpQAHmX4RBIsRKlQN2sgHIPTKL08eoTm4rWV5JKA8AZQALBABXABskVwRgKAAjaAB3WmB1dISIAEIPLhC3NAiY+KSUtMyoHJo8guLSnCA)): 17 | 18 | ```flow js 19 | const x: Identifier = { name: '', type: '' }; 20 | 21 | x.type = 'must NOT be writable!'; // No Flow error 22 | x.name = 'must NOT be writable!'; // No Flow error 23 | ``` 24 | 25 | This rule suggests to use `$ReadOnly<…>` to prevent accidental loss of readonly-ness: 26 | 27 | ```flow js 28 | type Identifier = $ReadOnly<{| 29 | ...INode, 30 | +name: string, 31 | |}>; 32 | 33 | const x: Identifier = { name: '', type: '' }; 34 | 35 | x.type = 'must NOT be writable!'; // $FlowExpectedError[cannot-write] 36 | x.name = 'must NOT be writable!'; // $FlowExpectedError[cannot-write] 37 | ``` 38 | 39 | 40 | -------------------------------------------------------------------------------- /.README/rules/valid-syntax.md: -------------------------------------------------------------------------------- 1 | ### `valid-syntax` 2 | 3 | **Deprecated** Babylon (the Babel parser) v6.10.0 fixes parsing of the invalid syntax this plugin warned against. 4 | 5 | Checks for simple Flow syntax errors. 6 | 7 | 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-object-rest-spread", 4 | "add-module-exports" 5 | ], 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "targets": { 11 | "node": 12 12 | } 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "plugin:eslint-plugin/recommended" 5 | ], 6 | "rules": { 7 | "eslint-plugin/require-meta-schema": 0, 8 | "eslint-plugin/require-meta-type": 0, 9 | "unicorn/prevent-abbreviations": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gajus 2 | patreon: gajus 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintrc 9 | !.gitignore 10 | !.npmignore 11 | !.README 12 | !.travis.yml 13 | /package-lock.json 14 | /yarn.lock 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 16 4 | - 12 5 | before_install: 6 | - npm config set depth 0 7 | notifications: 8 | email: false 9 | script: 10 | - npm run test 11 | - npm run lint 12 | - npm run build 13 | after_success: 14 | - export NODE_ENV=production 15 | - npm run build 16 | - semantic-release 17 | cache: 18 | npm: false 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **`README.md` is a generated file. Do not edit it directly.** Edit the files inside `.README` instead. 4 | 5 | ## Pre-Commit Hook 6 | 7 | When making a commit, the following Pre-Commit hooks run: 8 | 9 | * test and documentation checks 10 | * tests 11 | * lint 12 | * commit message validation (see "Commit Messages" below) 13 | 14 | ## Commit Messages 15 | 16 | All commit messages must begin with one of the following prefixes: 17 | 18 | * `fix: ` 19 | * `feat: ` 20 | * `refactor: ` 21 | * `docs: ` 22 | * `chore: ` 23 | 24 | The prefix is used to bump the correct segment of the version number during the automatic release. 25 | 26 | ## Tests 27 | 28 | Run them with `npm test`. 29 | 30 | ## Lint 31 | 32 | Run with `npm run lint`. 33 | 34 | ## Adding a Rule 35 | 36 | ### Source & Tests 37 | 38 | 1. Create a file in `tests/rules/assertions` named the `camelCase` version of your rule name with the following template: 39 | * `export default { invalid: [], valid: [] }` 40 | 2. Add your test file to `tests/rules/index.js` 41 | 3. Create a file in `src/rules` named the `camelCase` version of your rule name 42 | 4. Add your rule file to `src/index.js` 43 | 44 | ### Adding Documentation 45 | 46 | 1. Create new file in `./.README/rules/[rule-name].md`. 47 | * Use [./.README/rules/require-valid-file-annotation.md](./.README/rules/require-valid-file-annotation.md) as a template. 48 | * Ensure that rule documentation document includes `` declaration. 49 | 1. Update [./.README/README.md](/.README/README.md) to include the new rule. 50 | 1. Run `npm run create-readme` to generate the new `README.md` (you must be on `master` branch for this command to work) 51 | 52 | Note: Sections "The following patterns are considered problems:" and "The following patterns are not considered problems:" are **generated automatically** using the test cases. 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "dependencies": { 8 | "lodash": "^4.17.21", 9 | "string-natural-compare": "^3.0.1" 10 | }, 11 | "description": "Flowtype linting rules for ESLint.", 12 | "devDependencies": { 13 | "@babel/cli": "^7.15.7", 14 | "@babel/core": "^7.15.8", 15 | "@babel/eslint-parser": "^7.15.8", 16 | "@babel/node": "^7.15.8", 17 | "@babel/plugin-proposal-object-rest-spread": "^7.15.6", 18 | "@babel/plugin-syntax-flow": "^7.16.0", 19 | "@babel/plugin-syntax-jsx": "^7.16.0", 20 | "@babel/preset-env": "^7.15.8", 21 | "@babel/register": "^7.15.3", 22 | "ajv": "^8.6.3", 23 | "babel-plugin-add-module-exports": "^1.0.4", 24 | "eslint": "^8.1.0", 25 | "eslint-config-canonical": "^32.15.0", 26 | "eslint-plugin-eslint-plugin": "^4.0.2", 27 | "gitdown": "^3.1.4", 28 | "glob": "^7.2.0", 29 | "husky": "^7.0.4", 30 | "jsonlint": "^1.6.3", 31 | "mocha": "^9.1.3", 32 | "rimraf": "^3.0.2", 33 | "semantic-release": "^18.0.0" 34 | }, 35 | "engines": { 36 | "node": ">=12.0.0" 37 | }, 38 | "husky": { 39 | "hooks": { 40 | "pre-commit": "npm run check-docs && npm run check-tests && npm run lint && npm run test && npm run build && npm run format-json" 41 | } 42 | }, 43 | "keywords": [ 44 | "eslint", 45 | "plugin", 46 | "flowtype" 47 | ], 48 | "license": "BSD-3-Clause", 49 | "main": "./dist/index.js", 50 | "name": "eslint-plugin-flowtype", 51 | "peerDependencies": { 52 | "@babel/plugin-syntax-flow": "^7.14.5", 53 | "@babel/plugin-syntax-jsx": "^7.16.0", 54 | "eslint": "^8.1.0" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/gajus/eslint-plugin-flowtype" 59 | }, 60 | "scripts": { 61 | "build": "rimraf ./dist && babel ./src --out-dir ./dist --copy-files", 62 | "check-docs": "babel-node ./src/bin/checkDocs", 63 | "check-tests": "babel-node ./src/bin/checkTests", 64 | "create-readme": "gitdown ./.README/README.md --output-file ./README.md && npm run documentation-add-assertions", 65 | "documentation-add-assertions": "babel-node ./src/bin/addAssertions", 66 | "format-json": "jsonlint --sort-keys --in-place --indent \" \" ./src/configs/recommended.json", 67 | "lint": "eslint ./src ./tests", 68 | "test": "mocha --require @babel/register ./tests/rules/index.js" 69 | }, 70 | "version": "0.0.0-development" 71 | } 72 | -------------------------------------------------------------------------------- /src/bin/addAssertions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @file This script is used to inline assertions into the README.md documents. 5 | */ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import glob from 'glob'; 10 | import _ from 'lodash'; 11 | 12 | const formatCodeSnippet = (setup) => { 13 | const paragraphs = []; 14 | 15 | if (setup.options) { 16 | paragraphs.push('// Options: ' + JSON.stringify(setup.options)); 17 | } 18 | 19 | if (setup.settings) { 20 | paragraphs.push('// Settings: ' + JSON.stringify(setup.settings)); 21 | } 22 | 23 | paragraphs.push(setup.code); 24 | 25 | if (setup.errors) { 26 | for (const message of setup.errors) { 27 | paragraphs.push('// Message: ' + message.message); 28 | } 29 | } 30 | 31 | if (setup.rules) { 32 | paragraphs.push('// Additional rules: ' + JSON.stringify(setup.rules)); 33 | } 34 | 35 | return paragraphs.join('\n'); 36 | }; 37 | 38 | const getAssertions = () => { 39 | const assertionFiles = glob.sync(path.resolve(__dirname, '../../tests/rules/assertions/*.js')); 40 | 41 | const assertionNames = _.map(assertionFiles, (filePath) => { 42 | return path.basename(filePath, '.js'); 43 | }); 44 | 45 | const assertionCodes = _.map(assertionFiles, (filePath) => { 46 | // eslint-disable-next-line import/no-dynamic-require 47 | const codes = require(filePath); 48 | 49 | return { 50 | invalid: _.map(codes.invalid, formatCodeSnippet), 51 | valid: _.map(codes.valid, formatCodeSnippet), 52 | }; 53 | }); 54 | 55 | return _.zipObject(assertionNames, assertionCodes); 56 | }; 57 | 58 | const updateDocuments = (assertions) => { 59 | const readmeDocumentPath = path.join(__dirname, '../../README.md'); 60 | let documentBody; 61 | 62 | documentBody = fs.readFileSync(readmeDocumentPath, 'utf8'); 63 | 64 | documentBody = documentBody.replace(//ugi, (assertionsBlock) => { 65 | let exampleBody; 66 | 67 | const ruleName = assertionsBlock.match(/assertions ([a-z]+)/ui)[1]; 68 | 69 | const ruleAssertions = assertions[ruleName]; 70 | 71 | if (!ruleAssertions) { 72 | throw new Error('No assertions available for rule "' + ruleName + '".'); 73 | } 74 | 75 | exampleBody = ''; 76 | 77 | if (ruleAssertions.invalid.length) { 78 | exampleBody += 'The following patterns are considered problems:\n\n```js\n' + ruleAssertions.invalid.join('\n\n') + '\n```\n\n'; 79 | } 80 | 81 | if (ruleAssertions.valid.length) { 82 | exampleBody += 'The following patterns are not considered problems:\n\n```js\n' + ruleAssertions.valid.join('\n\n') + '\n```\n\n'; 83 | } 84 | 85 | return exampleBody; 86 | }); 87 | 88 | fs.writeFileSync(readmeDocumentPath, documentBody); 89 | }; 90 | 91 | updateDocuments(getAssertions()); 92 | -------------------------------------------------------------------------------- /src/bin/checkDocs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // @flow 4 | 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import { 8 | getRules, 9 | isFile, 10 | } from './utilities'; 11 | 12 | const windows = (array, size) => { 13 | const output = []; 14 | 15 | for (let ii = 0; ii < array.length - size + 1; ii++) { 16 | output.push(array.slice(ii, ii + size)); 17 | } 18 | 19 | return output; 20 | }; 21 | 22 | const getDocIndexRules = () => { 23 | const content = fs.readFileSync(path.resolve(__dirname, '../../.README/README.md'), 'utf-8'); 24 | 25 | const rules = content.split('\n').map((line) => { 26 | const match = /^{"gitdown": "include", "file": "([^"]+)"}$/u.exec(line); 27 | 28 | if (match === null) { 29 | return null; 30 | } 31 | 32 | return match[1].replace('./rules/', '').replace('.md', ''); 33 | }) 34 | .filter((rule) => { 35 | return rule !== null; 36 | }); 37 | 38 | if (rules.length === 0) { 39 | throw new Error('Docs checker is broken - it could not extract rules from docs index file.'); 40 | } 41 | 42 | return rules; 43 | }; 44 | 45 | const hasCorrectAssertions = (docPath, name) => { 46 | const content = fs.readFileSync(docPath, 'utf-8'); 47 | 48 | const match = //u.exec(content); 49 | 50 | if (match === null) { 51 | return false; 52 | } 53 | 54 | return match[1] === name; 55 | }; 56 | 57 | /** 58 | * Performed checks: 59 | * - file `/.README/rules/.md` exists 60 | * - file `/.README/rules/.md` contains correct assertions placeholder (``) 61 | * - rule is included in gitdown directive in `/.README/README.md` 62 | * - rules in `/.README/README.md` are alphabetically sorted 63 | */ 64 | const checkDocs = (rulesNames) => { 65 | const docIndexRules = getDocIndexRules(); 66 | 67 | const sorted = windows(docIndexRules, 2) 68 | .every((chunk) => { 69 | return chunk[0] < chunk[1]; 70 | }); 71 | 72 | if (!sorted) { 73 | throw new Error('Rules are not alphabetically sorted in `.README/README.md` file.'); 74 | } 75 | 76 | const invalid = rulesNames.filter((names) => { 77 | const docPath = path.resolve(__dirname, '../../.README/rules', names[1] + '.md'); 78 | const docExists = isFile(docPath); 79 | const inIndex = docIndexRules.includes(names[1]); 80 | const hasAssertions = docExists ? hasCorrectAssertions(docPath, names[0]) : false; 81 | 82 | return !(docExists && inIndex && hasAssertions); 83 | }); 84 | 85 | if (invalid.length > 0) { 86 | const invalidList = invalid 87 | .map((names) => { 88 | return names[0]; 89 | }).join(', '); 90 | 91 | throw new Error( 92 | 'Docs checker encountered an error in: ' + invalidList + '. ' + 93 | 'Make sure that for every rule you created documentation file with assertions placeholder in camelCase ' + 94 | 'and included the file path in `.README/README.md` file.', 95 | ); 96 | } 97 | }; 98 | 99 | checkDocs(getRules()); 100 | -------------------------------------------------------------------------------- /src/bin/checkTests.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { 6 | getRules, 7 | isFile, 8 | } from './utilities'; 9 | 10 | const getTestIndexRules = () => { 11 | const content = fs.readFileSync(path.resolve(__dirname, '../../tests/rules/index.js'), 'utf-8'); 12 | 13 | // eslint-disable-next-line unicorn/no-array-reduce 14 | const result = content.split('\n').reduce((acc, line) => { 15 | if (acc.inRulesArray) { 16 | if (line === '];') { 17 | acc.inRulesArray = false; 18 | } else { 19 | acc.rules.push(line.replace(/^\s*'([^']+)',?$/u, '$1')); 20 | } 21 | } else if (line === 'const reportingRules = [') { 22 | acc.inRulesArray = true; 23 | } 24 | 25 | return acc; 26 | }, { 27 | inRulesArray: false, 28 | rules: [], 29 | }); 30 | 31 | const {rules} = result; 32 | 33 | if (rules.length === 0) { 34 | throw new Error('Tests checker is broken - it could not extract rules from test index file.'); 35 | } 36 | 37 | return rules; 38 | }; 39 | 40 | /** 41 | * Performed checks: 42 | * - file `/tests/rules/assertions/.js` exists 43 | * - rule is included in `reportingRules` variable in `/tests/rules/index.js` 44 | */ 45 | const checkTests = (rulesNames) => { 46 | const testIndexRules = getTestIndexRules(); 47 | 48 | const invalid = rulesNames.filter((names) => { 49 | const testExists = isFile(path.resolve(__dirname, '../../tests/rules/assertions', names[0] + '.js')); 50 | const inIndex = testIndexRules.includes(names[1]); 51 | 52 | return !(testExists && inIndex); 53 | }); 54 | 55 | if (invalid.length > 0) { 56 | const invalidList = invalid.map((names) => { 57 | return names[0]; 58 | }).join(', '); 59 | 60 | throw new Error( 61 | 'Tests checker encountered an error in: ' + invalidList + '. ' + 62 | 'Make sure that for every rule you created test suite and included the rule name in `tests/rules/index.js` file.', 63 | ); 64 | } 65 | }; 66 | 67 | checkTests(getRules()); 68 | -------------------------------------------------------------------------------- /src/bin/utilities.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import glob from 'glob'; 6 | import _ from 'lodash'; 7 | 8 | export const getRules = () => { 9 | const rulesFiles = glob.sync(path.resolve(__dirname, '../rules/*.js')); 10 | 11 | const rulesNames = rulesFiles 12 | .map((file) => { 13 | return path.basename(file, '.js'); 14 | }) 15 | .map((name) => { 16 | return [name, _.kebabCase(name)]; 17 | }); 18 | 19 | return rulesNames; 20 | }; 21 | 22 | export const isFile = (filepath) => { 23 | try { 24 | return fs.statSync(filepath).isFile(); 25 | } catch { 26 | return false; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/configs/recommended.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { 4 | "babelOptions": { 5 | "plugins": [ 6 | "@babel/plugin-syntax-jsx", 7 | "@babel/plugin-syntax-flow" 8 | ] 9 | } 10 | }, 11 | "plugins": [ 12 | "flowtype" 13 | ], 14 | "rules": { 15 | "flowtype/boolean-style": [ 16 | 2, 17 | "boolean" 18 | ], 19 | "flowtype/define-flow-type": 1, 20 | "flowtype/delimiter-dangle": 0, 21 | "flowtype/generic-spacing": [ 22 | 2, 23 | "never" 24 | ], 25 | "flowtype/no-mixed": 0, 26 | "flowtype/no-types-missing-file-annotation": 2, 27 | "flowtype/no-weak-types": 0, 28 | "flowtype/require-parameter-type": 0, 29 | "flowtype/require-readonly-react-props": 0, 30 | "flowtype/require-return-type": 0, 31 | "flowtype/require-valid-file-annotation": 0, 32 | "flowtype/semi": 0, 33 | "flowtype/space-after-type-colon": [ 34 | 2, 35 | "always" 36 | ], 37 | "flowtype/space-before-generic-bracket": [ 38 | 2, 39 | "never" 40 | ], 41 | "flowtype/space-before-type-colon": [ 42 | 2, 43 | "never" 44 | ], 45 | "flowtype/type-id-match": 0, 46 | "flowtype/union-intersection-spacing": [ 47 | 2, 48 | "always" 49 | ], 50 | "flowtype/use-flow-type": 1 51 | }, 52 | "settings": { 53 | "flowtype": { 54 | "onlyFilesWithFlowAnnotation": false 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/rules/arrayStyle/index.js: -------------------------------------------------------------------------------- 1 | import isSimpleType from './isSimpleType'; 2 | import needWrap from './needWrap'; 3 | 4 | const schema = [ 5 | { 6 | enum: ['verbose', 'shorthand'], 7 | type: 'string', 8 | }, 9 | ]; 10 | 11 | const inlineType = (type) => { 12 | const inlined = type.replace(/\s+/ug, ' '); 13 | 14 | if (inlined.length <= 50) { 15 | return inlined; 16 | } 17 | 18 | return 'Type'; 19 | }; 20 | 21 | export default (defaultConfig, simpleType) => { 22 | const create = (context) => { 23 | const verbose = (context.options[0] || defaultConfig) === 'verbose'; 24 | 25 | return { 26 | // shorthand 27 | ArrayTypeAnnotation (node) { 28 | const rawElementType = context.getSourceCode().getText(node.elementType); 29 | const inlinedType = inlineType(rawElementType); 30 | const wrappedInlinedType = needWrap(node.elementType) ? '(' + inlinedType + ')' : inlinedType; 31 | 32 | if (isSimpleType(node.elementType) === simpleType && verbose) { 33 | context.report({ 34 | data: { 35 | type: inlinedType, 36 | wrappedType: wrappedInlinedType, 37 | }, 38 | fix (fixer) { 39 | return fixer.replaceText(node, 'Array<' + rawElementType + '>'); 40 | }, 41 | message: 'Use "Array<{{ type }}>", not "{{ wrappedType }}[]"', 42 | node, 43 | }); 44 | } 45 | }, 46 | 47 | // verbose 48 | GenericTypeAnnotation (node) { 49 | // Don't report on un-parameterized Array annotations. There are valid cases for this, 50 | // but regardless, we must NOT crash when encountering them. 51 | if (node.id.name === 'Array' && 52 | node.typeParameters && node.typeParameters.params.length === 1) { 53 | const elementTypeNode = node.typeParameters.params[0]; 54 | const rawElementType = context.getSourceCode().getText(elementTypeNode); 55 | const inlinedType = inlineType(rawElementType); 56 | const wrappedInlinedType = needWrap(elementTypeNode) ? '(' + inlinedType + ')' : inlinedType; 57 | 58 | if (isSimpleType(elementTypeNode) === simpleType && !verbose) { 59 | context.report({ 60 | data: { 61 | type: inlinedType, 62 | wrappedType: wrappedInlinedType, 63 | }, 64 | fix (fixer) { 65 | if (needWrap(elementTypeNode)) { 66 | return fixer.replaceText(node, '(' + rawElementType + ')[]'); 67 | } 68 | 69 | return fixer.replaceText(node, rawElementType + '[]'); 70 | }, 71 | message: 'Use "{{ wrappedType }}[]", not "Array<{{ type }}>"', 72 | node, 73 | }); 74 | } 75 | } 76 | }, 77 | }; 78 | }; 79 | 80 | return { 81 | create, 82 | meta: { 83 | fixable: 'code', 84 | }, 85 | schema, 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/rules/arrayStyle/isSimpleType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Types considered simple: 3 | * 4 | * - primitive types 5 | * - literal types 6 | * - mixed and any types 7 | * - generic types (such as Date, Promise, $Keys, etc.) 8 | * - array type written in shorthand notation 9 | * 10 | * Types not considered simple: 11 | * 12 | * - maybe type 13 | * - function type 14 | * - object type 15 | * - tuple type 16 | * - union and intersection types 17 | * 18 | * Reminder: if you change these semantics, don't forget to modify documentation of `array-style-...` rules 19 | */ 20 | 21 | const simpleTypePatterns = [ 22 | /^(?:Any|Array|Boolean|Generic|Mixed|Number|String|Void)TypeAnnotation$/u, 23 | /.+LiteralTypeAnnotation$/u, 24 | ]; 25 | 26 | export default (node) => { 27 | return simpleTypePatterns.some((pattern) => { 28 | return pattern.test(node.type); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/rules/arrayStyle/needWrap.js: -------------------------------------------------------------------------------- 1 | import isSimpleType from './isSimpleType'; 2 | 3 | const complexTypesWithoutWrap = new Set([ 4 | 'TupleTypeAnnotation', 5 | 'ObjectTypeAnnotation', 6 | ]); 7 | 8 | export default (node) => { 9 | return !isSimpleType(node) && !complexTypesWithoutWrap.has(node.type); 10 | }; 11 | -------------------------------------------------------------------------------- /src/rules/arrayStyleComplexType.js: -------------------------------------------------------------------------------- 1 | import makeArrayStyleRule from './arrayStyle'; 2 | 3 | export default makeArrayStyleRule('verbose', false); 4 | -------------------------------------------------------------------------------- /src/rules/arrayStyleSimpleType.js: -------------------------------------------------------------------------------- 1 | import makeArrayStyleRule from './arrayStyle'; 2 | 3 | export default makeArrayStyleRule('verbose', true); 4 | -------------------------------------------------------------------------------- /src/rules/booleanStyle.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['bool', 'boolean'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const create = (context) => { 9 | const longForm = (context.options[0] || 'boolean') === 'boolean'; 10 | 11 | return { 12 | BooleanTypeAnnotation (node) { 13 | const diff = node.range[1] - node.range[0]; 14 | 15 | if (longForm && diff === 4) { 16 | context.report({ 17 | fix (fixer) { 18 | return fixer.replaceText(node, 'boolean'); 19 | }, 20 | message: 'Use "boolean", not "bool"', 21 | node, 22 | }); 23 | } 24 | 25 | if (!longForm && diff !== 4) { 26 | context.report({ 27 | fix (fixer) { 28 | return fixer.replaceText(node, 'bool'); 29 | }, 30 | message: 'Use "bool", not "boolean"', 31 | node, 32 | }); 33 | } 34 | }, 35 | }; 36 | }; 37 | 38 | export default { 39 | create, 40 | meta: { 41 | fixable: 'code', 42 | }, 43 | schema, 44 | }; 45 | -------------------------------------------------------------------------------- /src/rules/defineFlowType.js: -------------------------------------------------------------------------------- 1 | const schema = []; 2 | 3 | const create = (context) => { 4 | let globalScope; 5 | 6 | // do nearly the same thing that eslint does for config globals 7 | // https://github.com/eslint/eslint/blob/v2.0.0/lib/eslint.js#L118-L194 8 | const makeDefined = (ident) => { 9 | let ii; 10 | 11 | // start from the right since we're going to remove items from the array 12 | for (ii = globalScope.through.length - 1; ii >= 0; ii--) { 13 | const ref = globalScope.through[ii]; 14 | 15 | if (ref.identifier.name === ident.name) { 16 | // use "__defineGeneric" since we don't have a reference to "escope.Variable" 17 | 18 | globalScope.__defineGeneric( 19 | ident.name, 20 | globalScope.set, 21 | globalScope.variables, 22 | ); 23 | const variable = globalScope.set.get(ident.name); 24 | 25 | variable.writeable = false; 26 | 27 | // "through" contains all references whose definition cannot be found 28 | // so we need to update references and remove the ones that were added 29 | globalScope.through.splice(ii, 1); 30 | ref.resolved = variable; 31 | variable.references.push(ref); 32 | } 33 | } 34 | }; 35 | 36 | return { 37 | ClassImplements (node) { 38 | makeDefined(node.id); 39 | }, 40 | DeclareInterface (node) { 41 | makeDefined(node.id); 42 | }, 43 | DeclareTypeAlias (node) { 44 | makeDefined(node.id); 45 | }, 46 | GenericTypeAnnotation (node) { 47 | if (node.id.type === 'Identifier') { 48 | makeDefined(node.id); 49 | } else if (node.id.type === 'QualifiedTypeIdentifier') { 50 | let qid; 51 | 52 | qid = node.id; 53 | do { 54 | qid = qid.qualification; 55 | } while (qid.qualification); 56 | 57 | makeDefined(qid); 58 | } 59 | }, 60 | 61 | // Can be removed once https://github.com/babel/babel-eslint/pull/696 is published 62 | OpaqueType (node) { 63 | if (node.id.type === 'Identifier') { 64 | makeDefined(node.id); 65 | } 66 | }, 67 | Program () { 68 | globalScope = context.getScope(); 69 | }, 70 | TypeParameterDeclaration (node) { 71 | for (const param of node.params) { 72 | makeDefined(param); 73 | } 74 | }, 75 | }; 76 | }; 77 | 78 | export default { 79 | create, 80 | schema, 81 | }; 82 | -------------------------------------------------------------------------------- /src/rules/delimiterDangle.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const schema = [ 4 | { 5 | enum: ['always', 'always-multiline', 'only-multiline', 'never'], 6 | type: 'string', 7 | }, 8 | { 9 | enum: ['always', 'always-multiline', 'only-multiline', 'never'], 10 | type: 'string', 11 | }, 12 | { 13 | enum: ['always', 'always-multiline', 'only-multiline', 'never'], 14 | type: 'string', 15 | }, 16 | ]; 17 | 18 | // required for reporting the correct position 19 | const getLast = (property, indexer) => { 20 | if (!property) { 21 | return indexer; 22 | } 23 | 24 | if (!indexer) { 25 | return property; 26 | } 27 | 28 | if (property.loc.end.line > indexer.loc.end.line) { 29 | return property; 30 | } 31 | 32 | if (indexer.loc.end.line > property.loc.end.line) { 33 | return indexer; 34 | } 35 | 36 | if (property.loc.end.column > indexer.loc.end.column) { 37 | return property; 38 | } 39 | 40 | return indexer; 41 | }; 42 | 43 | const create = (context) => { 44 | const option = context.options[0] || 'never'; 45 | const interfaceOption = context.options[1] || option; 46 | const inexactNotationOption = context.options[2] || 'never'; 47 | const sourceCode = context.getSourceCode(); 48 | 49 | const getNodeOption = (node) => { 50 | if (node.parent.type === 'InterfaceDeclaration') { 51 | return interfaceOption; 52 | } 53 | 54 | if (node.inexact) { 55 | return inexactNotationOption; 56 | } 57 | 58 | return option; 59 | }; 60 | 61 | const reporter = (node, message, fix) => { 62 | return () => { 63 | context.report({ 64 | fix, 65 | message, 66 | node, 67 | }); 68 | }; 69 | }; 70 | 71 | const makeReporters = (node, tokenToFix) => { 72 | return { 73 | dangle: reporter(node, 'Unexpected trailing delimiter', (fixer) => { 74 | return fixer.replaceText(tokenToFix, ''); 75 | }), 76 | noDangle: reporter(node, 'Missing trailing delimiter', (fixer) => { 77 | return fixer.insertTextAfter(tokenToFix, ','); 78 | }), 79 | }; 80 | }; 81 | 82 | const evaluate = (node, lastChildNode) => { 83 | if (!lastChildNode && !node.inexact) { 84 | return; 85 | } 86 | 87 | const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2); 88 | 89 | const isDangling = [';', ','].includes(penultimateToken.value); 90 | const isMultiLine = penultimateToken.loc.start.line !== lastToken.loc.start.line; 91 | 92 | // Use the object node if it's inexact since there's no child node for the inexact notation 93 | const report = makeReporters(node.inexact ? node : lastChildNode, penultimateToken); 94 | const nodeOption = getNodeOption(node); 95 | 96 | if (nodeOption === 'always' && !isDangling) { 97 | report.noDangle(); 98 | 99 | return; 100 | } 101 | 102 | if (nodeOption === 'never' && isDangling) { 103 | report.dangle(); 104 | 105 | return; 106 | } 107 | 108 | if (nodeOption === 'always-multiline' && !isDangling && isMultiLine) { 109 | report.noDangle(); 110 | 111 | return; 112 | } 113 | 114 | if (nodeOption === 'always-multiline' && isDangling && !isMultiLine) { 115 | report.dangle(); 116 | 117 | return; 118 | } 119 | 120 | if (nodeOption === 'only-multiline' && isDangling && !isMultiLine) { 121 | report.dangle(); 122 | } 123 | }; 124 | 125 | return { 126 | ObjectTypeAnnotation (node) { 127 | evaluate(node, getLast(_.last(node.properties), _.last(node.indexers))); 128 | }, 129 | 130 | TupleTypeAnnotation (node) { 131 | evaluate(node, _.last(node.types)); 132 | }, 133 | }; 134 | }; 135 | 136 | export default { 137 | create, 138 | meta: { 139 | fixable: 'code', 140 | }, 141 | schema, 142 | }; 143 | -------------------------------------------------------------------------------- /src/rules/enforceLineBreak.js: -------------------------------------------------------------------------------- 1 | const schema = []; 2 | 3 | const breakLineMessage = (direction) => { 4 | return `New line required ${direction} type declaration`; 5 | }; 6 | 7 | const create = (context) => { 8 | return { 9 | TypeAlias (node) { 10 | const sourceCode = context.getSourceCode(); 11 | if (sourceCode.lines.length === 1) { 12 | return; 13 | } 14 | 15 | const exportedType = node.parent.type === 'ExportNamedDeclaration'; 16 | const leadingComments = sourceCode.getCommentsBefore(exportedType ? node.parent : node); 17 | const hasLeadingComments = leadingComments.length > 0; 18 | 19 | if (node.loc.start.line !== 1) { 20 | if (hasLeadingComments && leadingComments[0].loc.start.line !== 1) { 21 | const lineAboveComment = sourceCode.lines[leadingComments[0].loc.start.line - 2]; 22 | if (lineAboveComment !== '') { 23 | context.report({ 24 | fix (fixer) { 25 | return fixer.insertTextBeforeRange(leadingComments[0].range, '\n'); 26 | }, 27 | message: breakLineMessage('above'), 28 | node, 29 | }); 30 | } 31 | } else if (!hasLeadingComments) { 32 | const isLineAbove = sourceCode.lines[node.loc.start.line - 2]; 33 | if (isLineAbove !== '') { 34 | context.report({ 35 | fix (fixer) { 36 | return fixer.insertTextBefore( 37 | exportedType ? node.parent : node, 38 | '\n', 39 | ); 40 | }, 41 | message: breakLineMessage('above'), 42 | node, 43 | }); 44 | } 45 | } 46 | } 47 | 48 | if (sourceCode.lines.length !== node.loc.end.line) { 49 | const isLineBelow = sourceCode.lines[node.loc.end.line]; 50 | if (isLineBelow !== '') { 51 | context.report({ 52 | fix (fixer) { 53 | return fixer.insertTextAfter(node, '\n'); 54 | }, 55 | message: breakLineMessage('below'), 56 | node, 57 | }); 58 | } 59 | } 60 | }, 61 | }; 62 | }; 63 | 64 | export default { 65 | create, 66 | meta: { 67 | fixable: 'code', 68 | }, 69 | schema, 70 | }; 71 | -------------------------------------------------------------------------------- /src/rules/genericSpacing.js: -------------------------------------------------------------------------------- 1 | import { 2 | spacingFixers, 3 | } from '../utilities'; 4 | 5 | const schema = [ 6 | { 7 | enum: ['always', 'never'], 8 | type: 'string', 9 | }, 10 | ]; 11 | 12 | const create = (context) => { 13 | const sourceCode = context.getSourceCode(); 14 | 15 | const never = (context.options[0] || 'never') === 'never'; 16 | 17 | return { 18 | GenericTypeAnnotation (node) { 19 | const types = node.typeParameters; 20 | 21 | // Promise 22 | // ^^^^^^^^^^^^ GenericTypeAnnotation (with typeParameters) 23 | // ^^^ GenericTypeAnnotation (without typeParameters) 24 | if (!types) { 25 | return; 26 | } 27 | 28 | const [opener, firstInnerToken] = sourceCode.getFirstTokens(types, 2); 29 | const [lastInnerToken, closer] = sourceCode.getLastTokens(types, 2); 30 | 31 | const spacesBefore = firstInnerToken.range[0] - opener.range[1]; 32 | const spacesAfter = closer.range[0] - lastInnerToken.range[1]; 33 | 34 | if (never) { 35 | if (spacesBefore) { 36 | const whiteSpaceBefore = sourceCode.text[opener.range[1]]; 37 | if (whiteSpaceBefore !== '\n' && whiteSpaceBefore !== '\r') { 38 | context.report({ 39 | data: {name: node.id.name}, 40 | fix: spacingFixers.stripSpacesAfter(opener, spacesBefore), 41 | message: 'There must be no space at start of "{{name}}" generic type annotation', 42 | node: types, 43 | }); 44 | } 45 | } 46 | 47 | if (spacesAfter) { 48 | const whiteSpaceAfter = sourceCode.text[closer.range[0] - 1]; 49 | if (whiteSpaceAfter !== '\n' && whiteSpaceAfter !== '\r') { 50 | context.report({ 51 | data: {name: node.id.name}, 52 | fix: spacingFixers.stripSpacesAfter(lastInnerToken, spacesAfter), 53 | message: 'There must be no space at end of "{{name}}" generic type annotation', 54 | node: types, 55 | }); 56 | } 57 | } 58 | } else { 59 | if (spacesBefore > 1) { 60 | context.report({ 61 | data: {name: node.id.name}, 62 | fix: spacingFixers.stripSpacesAfter(opener, spacesBefore - 1), 63 | message: 'There must be one space at start of "{{name}}" generic type annotation', 64 | node: types, 65 | }); 66 | } else if (spacesBefore === 0) { 67 | context.report({ 68 | data: {name: node.id.name}, 69 | fix: spacingFixers.addSpaceAfter(opener), 70 | message: 'There must be a space at start of "{{name}}" generic type annotation', 71 | node: types, 72 | }); 73 | } 74 | 75 | if (spacesAfter > 1) { 76 | context.report({ 77 | data: {name: node.id.name}, 78 | fix: spacingFixers.stripSpacesAfter(lastInnerToken, spacesAfter - 1), 79 | message: 'There must be one space at end of "{{name}}" generic type annotation', 80 | node: types, 81 | }); 82 | } else if (spacesAfter === 0) { 83 | context.report({ 84 | data: {name: node.id.name}, 85 | fix: spacingFixers.addSpaceAfter(lastInnerToken), 86 | message: 'There must be a space at end of "{{name}}" generic type annotation', 87 | node: types, 88 | }); 89 | } 90 | } 91 | }, 92 | }; 93 | }; 94 | 95 | const meta = { 96 | fixable: 'whitespace', 97 | }; 98 | 99 | export default { 100 | create, 101 | meta, 102 | schema, 103 | }; 104 | -------------------------------------------------------------------------------- /src/rules/interfaceIdMatch.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | type: 'string', 4 | }, 5 | ]; 6 | 7 | const create = (context) => { 8 | const pattern = new RegExp(context.options[0] || '^([A-Z][a-z0-9]*)+Type$', 'u'); 9 | 10 | const checkInterface = (interfaceDeclarationNode) => { 11 | const interfaceIdentifierName = interfaceDeclarationNode.id.name; 12 | 13 | if (!pattern.test(interfaceIdentifierName)) { 14 | context.report({data: { 15 | name: interfaceIdentifierName, 16 | pattern: pattern.toString(), 17 | }, 18 | message: 'Interface identifier \'{{name}}\' does not match pattern \'{{pattern}}\'.', 19 | node: interfaceDeclarationNode}); 20 | } 21 | }; 22 | 23 | return { 24 | InterfaceDeclaration: checkInterface, 25 | }; 26 | }; 27 | 28 | export default { 29 | create, 30 | schema, 31 | }; 32 | -------------------------------------------------------------------------------- /src/rules/newlineAfterFlowAnnotation.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const looksLikeFlowFileAnnotation = (comment) => { 4 | return /@(?:no)?flo/ui.test(comment); 5 | }; 6 | 7 | const schema = [ 8 | { 9 | enum: ['always', 'always-windows', 'never'], 10 | type: 'string', 11 | }, 12 | ]; 13 | 14 | const create = (context) => { 15 | const mode = context.options[0]; 16 | const never = mode === 'never'; 17 | 18 | const newline = mode === 'always-windows' ? '\r\n' : '\n'; 19 | 20 | return { 21 | Program (node) { 22 | const sourceCode = context.getSourceCode(); 23 | 24 | const potentialFlowFileAnnotation = _.find( 25 | context.getSourceCode().getAllComments(), 26 | (comment) => { 27 | return looksLikeFlowFileAnnotation(comment.value); 28 | }, 29 | ); 30 | 31 | if (potentialFlowFileAnnotation) { 32 | const {line} = potentialFlowFileAnnotation.loc.end; 33 | const nextLineIsEmpty = sourceCode.lines[line] === ''; 34 | 35 | if (!never && !nextLineIsEmpty) { 36 | context.report({ 37 | fix: (fixer) => { 38 | return fixer.insertTextAfter( 39 | potentialFlowFileAnnotation, 40 | newline, 41 | ); 42 | }, 43 | message: 'Expected newline after flow annotation', 44 | node, 45 | }); 46 | } 47 | 48 | if (never && nextLineIsEmpty) { 49 | context.report({ 50 | fix: (fixer) => { 51 | const lineBreak = sourceCode.text[potentialFlowFileAnnotation.range[1]]; 52 | 53 | return fixer.replaceTextRange( 54 | [ 55 | potentialFlowFileAnnotation.range[1], 56 | potentialFlowFileAnnotation.range[1] + ( 57 | lineBreak === '\r' ? 2 : 1 58 | ), 59 | ], 60 | '', 61 | ); 62 | }, 63 | message: 'Expected no newline after flow annotation', 64 | node, 65 | }); 66 | } 67 | } 68 | }, 69 | }; 70 | }; 71 | 72 | export default { 73 | create, 74 | meta: { 75 | fixable: 'code', 76 | }, 77 | schema, 78 | }; 79 | -------------------------------------------------------------------------------- /src/rules/noDupeKeys.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash/'; 2 | import { 3 | getParameterName, 4 | } from '../utilities'; 5 | 6 | const schema = []; 7 | 8 | const create = (context) => { 9 | const report = (node) => { 10 | context.report({ 11 | loc: node.loc, 12 | message: 'Duplicate property.', 13 | node, 14 | }); 15 | }; 16 | 17 | const analizeElement = (element) => { 18 | const {type} = element; 19 | let value; 20 | 21 | switch (type) { 22 | case 'GenericTypeAnnotation': 23 | value = element.id.name; 24 | break; 25 | case 'ObjectTypeAnnotation': 26 | // eslint-disable-next-line no-use-before-define 27 | value = builObjectStructure(element.properties); 28 | break; 29 | case 'TupleTypeAnnotation': 30 | // eslint-disable-next-line no-use-before-define 31 | value = buildArrayStructure(element.types); 32 | break; 33 | default: 34 | value = element.value; 35 | break; 36 | } 37 | 38 | return { 39 | type, 40 | value, 41 | }; 42 | }; 43 | 44 | const buildArrayStructure = (elements) => { 45 | return _.map(elements, (element) => { 46 | return analizeElement(element); 47 | }); 48 | }; 49 | 50 | const builObjectStructure = (properties) => { 51 | return _.map(properties, (property) => { 52 | const element = analizeElement( 53 | property.type === 'ObjectTypeSpreadProperty' ? 54 | property.argument : 55 | property.value, 56 | ); 57 | 58 | return { 59 | ...element, 60 | name: getParameterName(property, context), 61 | }; 62 | }); 63 | }; 64 | 65 | const checkForDuplicates = (node) => { 66 | const haystack = []; 67 | 68 | // filter out complex object types, like ObjectTypeSpreadProperty 69 | const identifierNodes = _.filter(node.properties, {type: 'ObjectTypeProperty'}); 70 | 71 | for (const identifierNode of identifierNodes) { 72 | const needle = {name: getParameterName(identifierNode, context)}; 73 | 74 | if (identifierNode.value.type === 'FunctionTypeAnnotation') { 75 | needle.args = _.map(identifierNode.value.params, (param) => { 76 | return analizeElement(param.typeAnnotation); 77 | }); 78 | } 79 | 80 | const match = _.some(haystack, (existingNeedle) => { 81 | return _.isEqual(existingNeedle, needle); 82 | }); 83 | 84 | if (match) { 85 | report(identifierNode); 86 | } else { 87 | haystack.push(needle); 88 | } 89 | } 90 | }; 91 | 92 | return { 93 | ObjectTypeAnnotation: checkForDuplicates, 94 | }; 95 | }; 96 | 97 | export default { 98 | create, 99 | schema, 100 | }; 101 | -------------------------------------------------------------------------------- /src/rules/noDuplicateTypeUnionIntersectionMembers.js: -------------------------------------------------------------------------------- 1 | const create = (context) => { 2 | const sourceCode = context.getSourceCode(); 3 | 4 | const { 5 | checkIntersections = true, 6 | checkUnions = true, 7 | } = context.options[1] || {}; 8 | 9 | const checkForDuplicates = (node) => { 10 | const uniqueMembers = []; 11 | const duplicates = []; 12 | 13 | const source = node.types.map((type) => { 14 | return { 15 | node: type, 16 | text: sourceCode.getText(type), 17 | }; 18 | }); 19 | 20 | const hasComments = node.types.some((type) => { 21 | const count = 22 | sourceCode.getCommentsBefore(type).length + 23 | sourceCode.getCommentsAfter(type).length; 24 | 25 | return count > 0; 26 | }); 27 | 28 | const fix = (fixer) => { 29 | const result = uniqueMembers 30 | .map((t) => { 31 | return t.text; 32 | }) 33 | .join( 34 | node.type === 'UnionTypeAnnotation' ? ' | ' : ' & ', 35 | ); 36 | 37 | return fixer.replaceText(node, result); 38 | }; 39 | 40 | for (const member of source) { 41 | const match = uniqueMembers.find((uniqueMember) => { 42 | return uniqueMember.text === member.text; 43 | }); 44 | 45 | if (match) { 46 | duplicates.push(member); 47 | } else { 48 | uniqueMembers.push(member); 49 | } 50 | } 51 | 52 | for (const duplicate of duplicates) { 53 | context.report({ 54 | data: { 55 | name: duplicate.text, 56 | type: node.type === 'UnionTypeAnnotation' ? 'union' : 'intersection', 57 | }, 58 | messageId: 'duplicate', 59 | node, 60 | 61 | // don't autofix if any of the types have leading/trailing comments 62 | // the logic for preserving them correctly is a pain - we may implement this later 63 | ...hasComments ? 64 | { 65 | suggest: [ 66 | { 67 | fix, 68 | messageId: 'suggestFix', 69 | }, 70 | ], 71 | } : 72 | {fix}, 73 | }); 74 | } 75 | }; 76 | 77 | return { 78 | IntersectionTypeAnnotation (node) { 79 | if (checkIntersections === true) { 80 | checkForDuplicates(node); 81 | } 82 | }, 83 | UnionTypeAnnotation (node) { 84 | if (checkUnions === true) { 85 | checkForDuplicates(node); 86 | } 87 | }, 88 | }; 89 | }; 90 | 91 | export default { 92 | create, 93 | meta: { 94 | fixable: 'code', 95 | messages: { 96 | duplicate: 'Duplicate {{type}} member found "{{name}}".', 97 | suggestFix: 'Remove duplicate members of type (removes all comments).', 98 | }, 99 | schema: [ 100 | { 101 | properties: { 102 | checkIntersections: { 103 | type: 'boolean', 104 | }, 105 | checkUnions: { 106 | type: 'boolean', 107 | }, 108 | }, 109 | type: 'object', 110 | }, 111 | ], 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /src/rules/noExistentialType.js: -------------------------------------------------------------------------------- 1 | // Support both node types for existential type 2 | // https://github.com/babel/babylon/issues/319 3 | const reporter = (context) => { 4 | return (node) => { 5 | context.report({ 6 | message: 'Unexpected use of existential type (*).', 7 | node, 8 | }); 9 | }; 10 | }; 11 | 12 | const create = (context) => { 13 | return { 14 | ExistentialTypeParam: reporter(context), 15 | ExistsTypeAnnotation: reporter(context), 16 | }; 17 | }; 18 | 19 | export default { 20 | create, 21 | }; 22 | -------------------------------------------------------------------------------- /src/rules/noFlowFixMeComments.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | type: 'string', 4 | }, 5 | ]; 6 | 7 | const message = '$FlowFixMe is treated as `any` and must be fixed.'; 8 | 9 | const isIdentifier = function (node, name) { 10 | return node && node.type === 'Identifier' && node.name.match(name); 11 | }; 12 | 13 | const create = (context) => { 14 | const allowedPattern = context.options[0] ? new RegExp(context.options[0], 'u') : null; 15 | const extraMessage = allowedPattern ? ' Fix it or match `' + allowedPattern.toString() + '`.' : ''; 16 | 17 | const passesExtraRegex = function (value) { 18 | if (!allowedPattern) { 19 | return false; 20 | } 21 | 22 | return value.match(allowedPattern); 23 | }; 24 | 25 | const handleComment = function (comment) { 26 | const value = comment.value.trim(); 27 | 28 | if (/\$FlowFixMe/u.test(value) && !passesExtraRegex(value)) { 29 | context.report({ 30 | message: message + extraMessage, 31 | node: comment, 32 | }); 33 | } 34 | }; 35 | 36 | return { 37 | GenericTypeAnnotation (node) { 38 | if (isIdentifier(node.id, /\$FlowFixMe/u)) { 39 | context.report({ 40 | message, 41 | node: node.id, 42 | }); 43 | } 44 | }, 45 | 46 | Program () { 47 | for (const comment of context 48 | .getSourceCode() 49 | .getAllComments() 50 | .filter((node) => { 51 | return node.type === 'Block' || node.type === 'Line'; 52 | })) { 53 | handleComment(comment); 54 | } 55 | }, 56 | }; 57 | }; 58 | 59 | export default { 60 | create, 61 | schema, 62 | }; 63 | -------------------------------------------------------------------------------- /src/rules/noInternalFlowType.js: -------------------------------------------------------------------------------- 1 | // We enumerate here all the React components Flow patches internally. It's because we don't want 2 | // to fail on otherwise valid type names (but rather take the actual implementation into account). 3 | // See: https://github.com/facebook/flow/blob/e23278bc17e6a0b5a2c52719d24b6bc5bb716931/src/services/code_action/insert_type_utils.ml#L607-L610 4 | const ReactComponents = [ 5 | 'AbstractComponent', 6 | 'ChildrenArray', 7 | 'ComponentType', 8 | 'Config', 9 | 'Context', 10 | 'Element', 11 | 'ElementConfig', 12 | 'ElementProps', 13 | 'ElementRef', 14 | 'ElementType', 15 | 'Key', 16 | 'Node', 17 | 'Portal', 18 | 'Ref', 19 | 'StatelessFunctionalComponent', 20 | ]; 21 | 22 | const create = (context) => { 23 | return { 24 | Identifier (node) { 25 | const match = node.name.match(/^React\$(?.+)/u); 26 | if (match !== null && match.groups !== null && match.groups !== undefined) { 27 | const {internalTypeName} = match.groups; 28 | if (ReactComponents.includes(internalTypeName)) { 29 | const validName = `React.${internalTypeName}`; 30 | context.report({ 31 | data: { 32 | invalidName: node.name, 33 | validName, 34 | }, 35 | message: 36 | 'Type identifier \'{{invalidName}}\' is not allowed. Use \'{{validName}}\' instead.', 37 | node, 38 | }); 39 | } 40 | } 41 | }, 42 | }; 43 | }; 44 | 45 | export default { 46 | create, 47 | }; 48 | -------------------------------------------------------------------------------- /src/rules/noMixed.js: -------------------------------------------------------------------------------- 1 | const schema = []; 2 | 3 | const create = (context) => { 4 | return { 5 | MixedTypeAnnotation (node) { 6 | context.report({ 7 | message: 'Unexpected use of mixed type', 8 | node, 9 | }); 10 | }, 11 | }; 12 | }; 13 | 14 | export default { 15 | create, 16 | schema, 17 | }; 18 | -------------------------------------------------------------------------------- /src/rules/noMutableArray.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const schema = []; 4 | 5 | // const x = []; 6 | const isEmptyArrayLiteral = (node) => { 7 | return _.get(node, 'init.type') === 'ArrayExpression' && _.get(node, 'init.elements.length') === 0; 8 | }; 9 | 10 | // const x = new Array(); const y = Array(); 11 | const isEmptyArrayInstance = (node) => { 12 | if (_.get(node, 'init.type') === 'NewExpression' || _.get(node, 'init.type') === 'CallExpression') { 13 | return _.get(node, 'init.callee.name') === 'Array' && _.get(node, 'init.arguments.length') === 0; 14 | } 15 | 16 | return false; 17 | }; 18 | 19 | const isAnnotationOfEmptyArrayInit = (node) => { 20 | if (_.has(node, 'parent.parent.parent')) { 21 | const parent = _.get(node, 'parent.parent.parent'); 22 | const isVariableDeclaration = _.get(parent, 'type') === 'VariableDeclarator'; 23 | 24 | return isVariableDeclaration && (isEmptyArrayLiteral(parent) || isEmptyArrayInstance(parent)); 25 | } 26 | 27 | return false; 28 | }; 29 | 30 | const create = (context) => { 31 | return { 32 | ArrayTypeAnnotation (node) { 33 | if (!isAnnotationOfEmptyArrayInit(node)) { 34 | context.report({ 35 | fix (fixer) { 36 | const rawElementType = context.getSourceCode().getText(node.elementType); 37 | 38 | return fixer.replaceText(node, '$ReadOnlyArray<' + rawElementType + '>'); 39 | }, 40 | message: 'Use "$ReadOnlyArray" instead of array shorthand notation', 41 | node, 42 | }); 43 | } 44 | }, 45 | GenericTypeAnnotation (node) { 46 | if (node.id.name === 'Array' && !isAnnotationOfEmptyArrayInit(node)) { 47 | context.report({ 48 | fix (fixer) { 49 | return fixer.replaceText(node.id, '$ReadOnlyArray'); 50 | }, 51 | message: 'Use "$ReadOnlyArray" instead of "Array"', 52 | node, 53 | }); 54 | } 55 | }, 56 | }; 57 | }; 58 | 59 | export default { 60 | create, 61 | meta: { 62 | fixable: 'code', 63 | }, 64 | schema, 65 | }; 66 | -------------------------------------------------------------------------------- /src/rules/noPrimitiveConstructorTypes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const schema = []; 4 | 5 | const create = (context) => { 6 | const regex = /^(Boolean|Number|String)$/u; 7 | 8 | return { 9 | GenericTypeAnnotation: (node) => { 10 | const name = _.get(node, 'id.name'); 11 | 12 | if (regex.test(name)) { 13 | context.report({ 14 | data: { 15 | name, 16 | }, 17 | loc: node.loc, 18 | message: 'Unexpected use of {{name}} constructor type.', 19 | node, 20 | }); 21 | } 22 | }, 23 | }; 24 | }; 25 | 26 | export default { 27 | create, 28 | schema, 29 | }; 30 | -------------------------------------------------------------------------------- /src/rules/noTypesMissingFileAnnotation.js: -------------------------------------------------------------------------------- 1 | import { 2 | isFlowFile, 3 | } from '../utilities'; 4 | 5 | /** 6 | * Disallows the use for flow types without a valid file annotation. 7 | * Only checks files without a valid flow annotation. 8 | */ 9 | 10 | const schema = []; 11 | 12 | const create = (context) => { 13 | // Skip flow files 14 | if (isFlowFile(context, false)) { 15 | return {}; 16 | } 17 | 18 | const reporter = (node, type) => { 19 | context.report({ 20 | data: {type}, 21 | message: 'Type {{type}} require valid Flow declaration.', 22 | node, 23 | }); 24 | }; 25 | 26 | return { 27 | ExportNamedDeclaration (node) { 28 | if (node.exportKind === 'type') { 29 | reporter(node, 'exports'); 30 | } 31 | }, 32 | ImportDeclaration (node) { 33 | if (node.importKind === 'type') { 34 | reporter(node, 'imports'); 35 | } 36 | 37 | if (node.importKind === 'value' && 38 | node.specifiers.some((specifier) => { 39 | return specifier.importKind === 'type'; 40 | })) { 41 | reporter(node, 'imports'); 42 | } 43 | }, 44 | TypeAlias (node) { 45 | reporter(node, 'aliases'); 46 | }, 47 | TypeAnnotation (node) { 48 | reporter(node, 'annotations'); 49 | }, 50 | }; 51 | }; 52 | 53 | export default { 54 | create, 55 | schema, 56 | }; 57 | -------------------------------------------------------------------------------- /src/rules/noUnusedExpressions.js: -------------------------------------------------------------------------------- 1 | // A wrapper around ESLint's core rule no-unused-expressions, additionally ignores type cast 2 | // expressions. 3 | 4 | import { 5 | getBuiltinRule, 6 | } from '../utilities/getBuiltinRule'; 7 | 8 | const noUnusedExpressionsRule = getBuiltinRule('no-unused-expressions'); 9 | 10 | const {meta} = noUnusedExpressionsRule; 11 | 12 | const create = (context) => { 13 | const coreChecks = noUnusedExpressionsRule.create(context); 14 | 15 | return { 16 | ExpressionStatement (node) { 17 | if ( 18 | node.expression.type === 'TypeCastExpression' || 19 | node.expression.type === 'OptionalCallExpression' 20 | ) { 21 | return; 22 | } 23 | 24 | // eslint-disable-next-line @babel/new-cap 25 | coreChecks.ExpressionStatement(node); 26 | }, 27 | }; 28 | }; 29 | 30 | export default { 31 | create, 32 | meta, 33 | }; 34 | -------------------------------------------------------------------------------- /src/rules/noWeakTypes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const schema = [ 4 | { 5 | additionalProperties: false, 6 | properties: { 7 | any: { 8 | type: 'boolean', 9 | }, 10 | Function: { 11 | type: 'boolean', 12 | }, 13 | Object: { 14 | type: 'boolean', 15 | }, 16 | }, 17 | type: 'object', 18 | }, 19 | ]; 20 | 21 | const reportWeakType = (context, weakType) => { 22 | return (node) => { 23 | context.report({ 24 | data: {weakType}, 25 | message: 'Unexpected use of weak type "{{weakType}}"', 26 | node, 27 | }); 28 | }; 29 | }; 30 | 31 | const genericTypeEvaluator = (context, {checkFunction, checkObject}) => { 32 | return (node) => { 33 | const name = _.get(node, 'id.name'); 34 | 35 | if (checkFunction && name === 'Function' || checkObject && name === 'Object') { 36 | reportWeakType(context, name)(node); 37 | } 38 | }; 39 | }; 40 | 41 | const create = (context) => { 42 | const checkAny = _.get(context, 'options[0].any', true) === true; 43 | const checkFunction = _.get(context, 'options[0].Function', true) === true; 44 | const checkObject = _.get(context, 'options[0].Object', true) === true; 45 | 46 | const checks = {}; 47 | 48 | if (checkAny) { 49 | checks.AnyTypeAnnotation = reportWeakType(context, 'any'); 50 | } 51 | 52 | if (checkFunction || checkObject) { 53 | checks.GenericTypeAnnotation = genericTypeEvaluator(context, { 54 | checkFunction, 55 | checkObject, 56 | }); 57 | } 58 | 59 | return checks; 60 | }; 61 | 62 | export default { 63 | create, 64 | schema, 65 | }; 66 | -------------------------------------------------------------------------------- /src/rules/objectTypeCurlySpacing.js: -------------------------------------------------------------------------------- 1 | import { 2 | spacingFixers, 3 | } from '../utilities'; 4 | 5 | const schema = [ 6 | { 7 | enum: ['always', 'never'], 8 | type: 'string', 9 | }, 10 | ]; 11 | 12 | const meta = { 13 | fixable: 'code', 14 | }; 15 | 16 | const sameLine = (left, right) => { 17 | return left.loc.end.line === right.loc.start.line; 18 | }; 19 | 20 | const create = (context) => { 21 | const never = (context?.options[0] ?? 'never') === 'never'; 22 | const sourceCode = context.getSourceCode(); 23 | 24 | return { 25 | ObjectTypeAnnotation (node) { 26 | const { 27 | properties, 28 | } = node; 29 | 30 | if (properties.length === 0) { 31 | return; 32 | } 33 | 34 | const [opener, firstInnerToken] = sourceCode.getFirstTokens(node, 2); 35 | const [lastInnerToken, closer] = sourceCode.getLastTokens(node, 2); 36 | 37 | const spacesBefore = firstInnerToken.range[0] - opener.range[1]; 38 | const spacesAfter = closer.range[0] - lastInnerToken.range[1]; 39 | 40 | // Check the opening brace 41 | if (sameLine(opener, firstInnerToken)) { 42 | if (never && spacesBefore) { 43 | context.report({ 44 | data: { 45 | token: opener.value, 46 | }, 47 | fix: spacingFixers.stripSpacesAfter(opener, spacesBefore), 48 | message: 'There must be no space after "{{token}}".', 49 | node, 50 | }); 51 | } else if (!never && !spacesBefore) { 52 | context.report({ 53 | data: { 54 | token: opener.value, 55 | }, 56 | fix: spacingFixers.addSpaceAfter(opener), 57 | message: 'A space is required after "{{token}}".', 58 | node, 59 | }); 60 | } 61 | } 62 | 63 | // Check the closing brace 64 | if (sameLine(lastInnerToken, closer)) { 65 | if (never && spacesAfter) { 66 | context.report({ 67 | data: { 68 | token: closer.value, 69 | }, 70 | fix: spacingFixers.stripSpacesBefore(closer, spacesAfter), 71 | message: 'There must be no space before "{{token}}".', 72 | node, 73 | }); 74 | } else if (!never && !spacesAfter) { 75 | context.report({ 76 | data: { 77 | token: closer.value, 78 | }, 79 | fix: spacingFixers.addSpaceAfter(lastInnerToken), 80 | message: 'A space is required before "{{token}}".', 81 | node, 82 | }); 83 | } 84 | } 85 | }, 86 | }; 87 | }; 88 | 89 | export default { 90 | create, 91 | meta, 92 | schema, 93 | }; 94 | -------------------------------------------------------------------------------- /src/rules/objectTypeDelimiter.js: -------------------------------------------------------------------------------- 1 | // ported from babel/flow-object-type; original author: Nat Mote 2 | // https://github.com/babel/eslint-plugin-babel/blob/c0a49d25a97feb12c1d07073a0b37317359a5fe5/rules/flow-object-type.js 3 | 4 | const SEMICOLON = { 5 | char: ';', 6 | name: 'semicolon', 7 | }; 8 | 9 | const COMMA = { 10 | char: ',', 11 | name: 'comma', 12 | }; 13 | 14 | const create = (context) => { 15 | let GOOD; 16 | let BAD; 17 | 18 | if (!context.options[0] || context.options[0] === COMMA.name) { 19 | GOOD = COMMA; 20 | BAD = SEMICOLON; 21 | } else { 22 | GOOD = SEMICOLON; 23 | BAD = COMMA; 24 | } 25 | 26 | const requireProperPunctuation = (node) => { 27 | const sourceCode = context.getSourceCode(); 28 | const tokens = sourceCode.getTokens(node); 29 | let lastToken; 30 | 31 | lastToken = tokens[tokens.length - 1]; 32 | if (lastToken.type !== 'Punctuator' || 33 | !(lastToken.value === SEMICOLON.char || 34 | lastToken.value === COMMA.char)) { 35 | const parentTokens = sourceCode.getTokens(node.parent); 36 | 37 | lastToken = parentTokens[parentTokens.indexOf(lastToken) + 1]; 38 | } 39 | 40 | if (lastToken.type === 'Punctuator' && lastToken.value === BAD.char) { 41 | context.report({ 42 | fix (fixer) { 43 | return fixer.replaceText(lastToken, GOOD.char); 44 | }, 45 | message: 'Prefer ' + GOOD.name + 's to ' + BAD.name + 's in object and class types', 46 | node: lastToken, 47 | }); 48 | } 49 | }; 50 | 51 | return { 52 | ObjectTypeCallProperty: requireProperPunctuation, 53 | ObjectTypeIndexer: requireProperPunctuation, 54 | ObjectTypeProperty: requireProperPunctuation, 55 | }; 56 | }; 57 | 58 | const schema = [ 59 | { 60 | enum: ['semicolon', 'comma'], 61 | type: 'string', 62 | }, 63 | ]; 64 | 65 | export default { 66 | create, 67 | meta: { 68 | fixable: 'code', 69 | }, 70 | schema, 71 | }; 72 | -------------------------------------------------------------------------------- /src/rules/quotes.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['double', 'single'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const meta = { 9 | fixable: 'code', 10 | }; 11 | 12 | const create = (context) => { 13 | const double = (context.options[0] || 'double') === 'double'; 14 | const sourceCode = context.getSourceCode(); 15 | 16 | return { 17 | StringLiteralTypeAnnotation (node) { 18 | if (double && sourceCode.text[node.range[0]] !== '"') { 19 | // double 20 | context.report({ 21 | fix: (fixer) => { 22 | return [ 23 | fixer.replaceTextRange([node.range[0], node.range[0] + 1], '"'), 24 | fixer.replaceTextRange([node.range[1] - 1, node.range[1]], '"'), 25 | ]; 26 | }, 27 | message: 'String literals must use double quote.', 28 | node, 29 | }); 30 | } else if (!double && sourceCode.text[node.range[0]] !== '\'') { 31 | // single 32 | context.report({ 33 | fix: (fixer) => { 34 | return [ 35 | fixer.replaceTextRange([node.range[0], node.range[0] + 1], '\''), 36 | fixer.replaceTextRange([node.range[1] - 1, node.range[1]], '\''), 37 | ]; 38 | }, 39 | message: 'String literals must use single quote.', 40 | node, 41 | }); 42 | } 43 | }, 44 | }; 45 | }; 46 | 47 | export default { 48 | create, 49 | meta, 50 | schema, 51 | }; 52 | -------------------------------------------------------------------------------- /src/rules/requireCompoundTypeAlias.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['always', 'never'], 4 | type: 'string', 5 | }, 6 | { 7 | additionalProperties: false, 8 | properties: { 9 | allowNull: { 10 | type: 'boolean', 11 | }, 12 | }, 13 | type: 'object', 14 | }, 15 | ]; 16 | 17 | const create = (context) => { 18 | const always = (context.options[0] || 'always') === 'always'; 19 | 20 | const allowNull = !(context.options[1] && context.options[1].allowNull === false); 21 | 22 | if (always) { 23 | return { 24 | IntersectionTypeAnnotation (node) { 25 | if ( 26 | allowNull && 27 | node.types.length === 2 && 28 | ( 29 | node.types[0].type === 'NullLiteralTypeAnnotation' || 30 | node.types[1].type === 'NullLiteralTypeAnnotation' 31 | ) 32 | ) { 33 | return; 34 | } 35 | 36 | if (node.parent.type !== 'TypeAlias') { 37 | context.report({ 38 | message: 'All intersection types must be declared with named type alias.', 39 | node, 40 | }); 41 | } 42 | }, 43 | UnionTypeAnnotation (node) { 44 | if ( 45 | allowNull && 46 | node.types.length === 2 && 47 | ( 48 | node.types[0].type === 'NullLiteralTypeAnnotation' || 49 | node.types[1].type === 'NullLiteralTypeAnnotation' 50 | ) 51 | ) { 52 | return; 53 | } 54 | 55 | if (node.parent.type !== 'TypeAlias') { 56 | context.report({ 57 | message: 'All union types must be declared with named type alias.', 58 | node, 59 | }); 60 | } 61 | }, 62 | }; 63 | } 64 | 65 | return {}; 66 | }; 67 | 68 | export default { 69 | create, 70 | schema, 71 | }; 72 | -------------------------------------------------------------------------------- /src/rules/requireExactType.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['always', 'never'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const meta = { 9 | fixable: 'code', 10 | }; 11 | 12 | const create = (context) => { 13 | const always = (context.options[0] || 'always') === 'always'; 14 | const sourceCode = context.getSourceCode(); 15 | 16 | return { 17 | ObjectTypeAnnotation (node) { 18 | const { 19 | exact, 20 | indexers, 21 | inexact, 22 | } = node; 23 | 24 | if (!['DeclareClass', 'InterfaceDeclaration'].includes(node.parent.type) && always && !exact && !inexact && indexers.length === 0) { 25 | context.report({ 26 | fix: (fixer) => { 27 | return [ 28 | fixer.replaceText(sourceCode.getFirstToken(node), '{|'), 29 | fixer.replaceText(sourceCode.getLastToken(node), '|}'), 30 | ]; 31 | }, 32 | message: 'Object type must be exact.', 33 | node, 34 | }); 35 | } 36 | 37 | if (!always && exact) { 38 | context.report({ 39 | fix: (fixer) => { 40 | return [ 41 | fixer.replaceText(sourceCode.getFirstToken(node), '{'), 42 | fixer.replaceText(sourceCode.getLastToken(node), '}'), 43 | ]; 44 | }, 45 | message: 'Object type must not be exact.', 46 | node, 47 | }); 48 | } 49 | }, 50 | }; 51 | }; 52 | 53 | export default { 54 | create, 55 | meta, 56 | schema, 57 | }; 58 | -------------------------------------------------------------------------------- /src/rules/requireIndexerName.js: -------------------------------------------------------------------------------- 1 | import { 2 | getParameterName, 3 | } from '../utilities'; 4 | 5 | const schema = [ 6 | { 7 | enum: ['always', 'never'], 8 | type: 'string', 9 | }, 10 | ]; 11 | 12 | const create = (context) => { 13 | const always = (context.options[0] || 'always') === 'always'; 14 | 15 | if (always) { 16 | return { 17 | ObjectTypeIndexer (node) { 18 | const id = getParameterName(node, context); 19 | const rawKeyType = context.getSourceCode().getText(node.key); 20 | if (id === null) { 21 | context.report({ 22 | fix (fixer) { 23 | return fixer.replaceText(node.key, 'key: ' + rawKeyType); 24 | }, 25 | message: 'All indexers must be declared with key name.', 26 | node, 27 | }); 28 | } 29 | }, 30 | }; 31 | } 32 | 33 | return {}; 34 | }; 35 | 36 | export default { 37 | create, 38 | meta: { 39 | fixable: 'code', 40 | }, 41 | schema, 42 | }; 43 | -------------------------------------------------------------------------------- /src/rules/requireInexactType.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['always', 'never'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const create = (context) => { 9 | const always = (context.options[0] || 'always') === 'always'; 10 | 11 | return { 12 | ObjectTypeAnnotation (node) { 13 | const {inexact, exact} = node; 14 | 15 | if (!Object.prototype.hasOwnProperty.call(node, 'inexact')) { 16 | return; 17 | } 18 | 19 | if (always && !inexact && !exact) { 20 | context.report({ 21 | message: 'Type must be explicit inexact.', 22 | node, 23 | }); 24 | } 25 | 26 | if (!always && inexact) { 27 | context.report({ 28 | message: 'Type must not be explicit inexact.', 29 | node, 30 | }); 31 | } 32 | }, 33 | }; 34 | }; 35 | 36 | export default { 37 | create, 38 | schema, 39 | }; 40 | -------------------------------------------------------------------------------- /src/rules/requireParameterType.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | getParameterName, 4 | iterateFunctionNodes, 5 | quoteName, 6 | } from '../utilities'; 7 | 8 | const schema = [ 9 | { 10 | additionalProperties: false, 11 | properties: { 12 | excludeArrowFunctions: { 13 | enum: [false, true, 'expressionsOnly'], 14 | }, 15 | excludeParameterMatch: { 16 | type: 'string', 17 | }, 18 | }, 19 | type: 'object', 20 | }, 21 | ]; 22 | 23 | const create = iterateFunctionNodes((context) => { 24 | const skipArrows = _.get(context, 'options[0].excludeArrowFunctions'); 25 | const excludeParameterMatch = new RegExp(_.get(context, 'options[0].excludeParameterMatch', 'a^'), 'u'); 26 | 27 | return (functionNode) => { 28 | // It is save to ignore FunctionTypeAnnotation nodes in this rule. 29 | if (functionNode.type === 'FunctionTypeAnnotation') { 30 | return; 31 | } 32 | 33 | const isArrow = functionNode.type === 'ArrowFunctionExpression'; 34 | const isArrowFunctionExpression = functionNode.expression; 35 | const functionAnnotation = isArrow && _.get(functionNode, 'parent.id.typeAnnotation'); 36 | 37 | if (skipArrows === 'expressionsOnly' && isArrowFunctionExpression || skipArrows === true && isArrow) { 38 | return; 39 | } 40 | 41 | // eslint-disable-next-line unicorn/no-array-for-each 42 | _.forEach(functionNode.params, (identifierNode) => { 43 | const parameterName = getParameterName(identifierNode, context); 44 | 45 | if (excludeParameterMatch.test(parameterName)) { 46 | return; 47 | } 48 | 49 | let typeAnnotation; 50 | 51 | typeAnnotation = _.get(identifierNode, 'typeAnnotation') || _.get(identifierNode, 'left.typeAnnotation'); 52 | 53 | if (isArrow && functionAnnotation) { 54 | typeAnnotation = true; 55 | } 56 | 57 | if (!typeAnnotation) { 58 | context.report({ 59 | data: { 60 | name: quoteName(parameterName), 61 | }, 62 | message: 'Missing {{name}}parameter type annotation.', 63 | node: identifierNode, 64 | }); 65 | } 66 | }); 67 | }; 68 | }); 69 | 70 | export default { 71 | create, 72 | schema, 73 | }; 74 | -------------------------------------------------------------------------------- /src/rules/requireTypesAtTop.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const schema = [ 4 | { 5 | enum: ['always', 'never'], 6 | type: 'string', 7 | }, 8 | ]; 9 | 10 | const create = (context) => { 11 | const always = (context.options[0] || 'always') === 'always'; 12 | 13 | if (always) { 14 | const sourceCode = context.getSourceCode(); 15 | 16 | // nodes representing type and import declarations 17 | const ignoredNodes = [ 18 | // import ... 19 | (node) => { 20 | return node.type === 'ImportDeclaration'; 21 | }, 22 | 23 | // export type Foo = ... 24 | // export opaque type Foo = ... 25 | // export type Foo from ... 26 | // export opaque type Foo from ... 27 | (node) => { 28 | return node.type === 'ExportNamedDeclaration' && node.exportKind === 'type'; 29 | }, 30 | 31 | // type Foo = ... 32 | (node) => { 33 | return node.type === 'TypeAlias'; 34 | }, 35 | 36 | // opaque type Foo = ... 37 | (node) => { 38 | return node.type === 'OpaqueType'; 39 | }, 40 | ]; 41 | 42 | const isIgnoredNode = (node) => { 43 | for (const predicate of ignoredNodes) { 44 | if (predicate(node)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | }; 51 | 52 | let regularCodeStartRange; 53 | 54 | for (const node of sourceCode.ast.body) { 55 | if (!isIgnoredNode(node)) { 56 | regularCodeStartRange = node.range; 57 | break; 58 | } 59 | } 60 | 61 | if (!_.isArray(regularCodeStartRange)) { 62 | // a source with only ignored nodes 63 | return {}; 64 | } 65 | 66 | return { 67 | 'TypeAlias, OpaqueType' (node) { 68 | if (node.range[0] > regularCodeStartRange[0]) { 69 | context.report({ 70 | message: 'All type declaration must be at the top of the file, after any import declarations.', 71 | node, 72 | }); 73 | } 74 | }, 75 | }; 76 | } 77 | 78 | return {}; 79 | }; 80 | 81 | export default { 82 | create, 83 | schema, 84 | }; 85 | -------------------------------------------------------------------------------- /src/rules/requireVariableType.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | isFlowFile, 4 | quoteName, 5 | } from '../utilities'; 6 | 7 | const schema = [ 8 | { 9 | additionalProperties: false, 10 | properties: { 11 | excludeVariableMatch: { 12 | type: 'string', 13 | }, 14 | excludeVariableTypes: { 15 | additionalProperties: false, 16 | properties: { 17 | const: { 18 | type: 'boolean', 19 | }, 20 | let: { 21 | type: 'boolean', 22 | }, 23 | var: { 24 | type: 'boolean', 25 | }, 26 | }, 27 | type: 'object', 28 | }, 29 | }, 30 | type: 'object', 31 | }, 32 | ]; 33 | 34 | const create = (context) => { 35 | const checkThisFile = !_.get(context, 'settings.flowtype.onlyFilesWithFlowAnnotation') || isFlowFile(context); 36 | 37 | if (!checkThisFile) { 38 | return () => {}; 39 | } 40 | 41 | const excludeVariableMatch = new RegExp(_.get(context, 'options[0].excludeVariableMatch', 'a^'), 'u'); 42 | const excludeVariableTypes = _.get(context, 'options[0].excludeVariableTypes', {}); 43 | 44 | return { 45 | VariableDeclaration: (variableDeclaration) => { 46 | const variableType = _.get(variableDeclaration, 'kind'); 47 | 48 | if (_.get(excludeVariableTypes, variableType)) { 49 | return; 50 | } 51 | 52 | // eslint-disable-next-line unicorn/no-array-for-each 53 | _.forEach(variableDeclaration.declarations, (variableDeclarator) => { 54 | const identifierNode = _.get(variableDeclarator, 'id'); 55 | const identifierName = _.get(identifierNode, 'name'); 56 | 57 | if (excludeVariableMatch.test(identifierName)) { 58 | return; 59 | } 60 | 61 | const typeAnnotation = _.get(identifierNode, 'typeAnnotation'); 62 | 63 | if (!typeAnnotation) { 64 | context.report({ 65 | data: { 66 | name: quoteName(identifierName), 67 | }, 68 | message: 'Missing {{name}}variable type annotation.', 69 | node: identifierNode, 70 | }); 71 | } 72 | }); 73 | }, 74 | }; 75 | }; 76 | 77 | export default { 78 | create, 79 | schema, 80 | }; 81 | -------------------------------------------------------------------------------- /src/rules/semi.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['always', 'never'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const isSemicolon = (token) => { 9 | return token.type === 'Punctuator' && token.value === ';'; 10 | }; 11 | 12 | const create = (context) => { 13 | const never = (context.options[0] || 'always') === 'never'; 14 | const sourceCode = context.getSourceCode(); 15 | 16 | const report = (node, missing) => { 17 | const lastToken = sourceCode.getLastToken(node); 18 | let fix; 19 | let message; 20 | let {loc} = lastToken; 21 | 22 | if (missing) { 23 | message = 'Missing semicolon.'; 24 | loc = loc.end; 25 | fix = (fixer) => { 26 | return fixer.insertTextAfter(lastToken, ';'); 27 | }; 28 | } else { 29 | message = 'Extra semicolon.'; 30 | loc = loc.start; 31 | fix = (fixer) => { 32 | return fixer.remove(lastToken); 33 | }; 34 | } 35 | 36 | context.report({ 37 | fix, 38 | loc, 39 | message, 40 | node, 41 | }); 42 | }; 43 | 44 | const checkForSemicolon = (node) => { 45 | const lastToken = sourceCode.getLastToken(node); 46 | const isLastTokenSemicolon = isSemicolon(lastToken); 47 | 48 | if (never && isLastTokenSemicolon) { 49 | report(node, false); 50 | } 51 | 52 | if (!never && !isLastTokenSemicolon) { 53 | report(node, true); 54 | } 55 | }; 56 | 57 | return { 58 | OpaqueType: checkForSemicolon, 59 | TypeAlias: checkForSemicolon, 60 | TypeAnnotation: (node) => { 61 | if (node.parent.type === 'ClassProperty') { 62 | checkForSemicolon(node.parent); 63 | } 64 | }, 65 | }; 66 | }; 67 | 68 | export default { 69 | create, 70 | meta: { 71 | fixable: 'code', 72 | }, 73 | schema, 74 | }; 75 | -------------------------------------------------------------------------------- /src/rules/spaceAfterTypeColon.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import makeSpacing from './typeColonSpacing'; 3 | 4 | const schema = [ 5 | { 6 | enum: ['always', 'never'], 7 | type: 'string', 8 | }, 9 | { 10 | additionalProperties: false, 11 | properties: { 12 | allowLineBreak: { 13 | type: 'boolean', 14 | }, 15 | }, 16 | type: 'object', 17 | }, 18 | ]; 19 | 20 | const create = (context) => { 21 | return makeSpacing('after', context, { 22 | allowLineBreak: _.get(context, ['options', '1', 'allowLineBreak'], false), 23 | always: _.get(context, ['options', '0'], 'always') === 'always', 24 | }); 25 | }; 26 | 27 | export default { 28 | create, 29 | meta: { 30 | fixable: 'code', 31 | }, 32 | schema, 33 | }; 34 | -------------------------------------------------------------------------------- /src/rules/spaceBeforeGenericBracket.js: -------------------------------------------------------------------------------- 1 | import { 2 | spacingFixers, 3 | } from '../utilities'; 4 | 5 | const schema = [ 6 | { 7 | enum: ['always', 'never'], 8 | type: 'string', 9 | }, 10 | ]; 11 | 12 | const create = (context) => { 13 | const never = (context.options[0] || 'never') === 'never'; 14 | 15 | return { 16 | GenericTypeAnnotation (node) { 17 | const types = node.typeParameters; 18 | 19 | // Promise 20 | // ^^^^^^^^^^^^ GenericTypeAnnotation (with typeParameters) 21 | // ^^^ GenericTypeAnnotation (without typeParameters) 22 | if (!types) { 23 | return; 24 | } 25 | 26 | const spaceBefore = types.range[0] - node.id.range[1]; 27 | 28 | if (never && spaceBefore) { 29 | context.report({ 30 | data: {name: node.id.name}, 31 | fix: spacingFixers.stripSpacesAfter(node.id, spaceBefore), 32 | message: 'There must be no space before "{{name}}" generic type annotation bracket', 33 | node, 34 | }); 35 | } 36 | 37 | if (!never && !spaceBefore) { 38 | context.report({ 39 | data: {name: node.id.name}, 40 | fix: spacingFixers.addSpaceAfter(node.id), 41 | message: 'There must be a space before "{{name}}" generic type annotation bracket', 42 | node, 43 | }); 44 | } 45 | 46 | if (!never && spaceBefore > 1) { 47 | context.report({ 48 | data: {name: node.id.name}, 49 | fix: spacingFixers.stripSpacesAfter(node.id, spaceBefore - 1), 50 | message: 'There must be one space before "{{name}}" generic type annotation bracket', 51 | node, 52 | }); 53 | } 54 | }, 55 | }; 56 | }; 57 | 58 | export default { 59 | create, 60 | meta: { 61 | fixable: 'code', 62 | }, 63 | schema, 64 | }; 65 | -------------------------------------------------------------------------------- /src/rules/spaceBeforeTypeColon.js: -------------------------------------------------------------------------------- 1 | import makeSpacing from './typeColonSpacing'; 2 | 3 | const schema = [ 4 | { 5 | enum: ['always', 'never'], 6 | type: 'string', 7 | }, 8 | ]; 9 | 10 | const create = (context) => { 11 | return makeSpacing('before', context, { 12 | always: context.options[0] === 'always', 13 | }); 14 | }; 15 | 16 | export default { 17 | create, 18 | meta: { 19 | fixable: 'code', 20 | }, 21 | schema, 22 | }; 23 | -------------------------------------------------------------------------------- /src/rules/spreadExactType.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['always', 'never'], 4 | type: 'string', 5 | }, 6 | ]; 7 | 8 | const create = (context) => { 9 | return { 10 | ObjectTypeAnnotation (node) { 11 | const {properties} = node; 12 | 13 | for (const property of properties) { 14 | const {type} = property; 15 | if (type === 'ObjectTypeSpreadProperty') { 16 | const {argument: {type: argumentType, id: argumentId}} = property; 17 | if ( 18 | argumentType !== 'GenericTypeAnnotation' || argumentId.name !== '$Exact') { 19 | context.report({ 20 | message: 'Use $Exact to make type spreading safe.', 21 | node, 22 | }); 23 | } 24 | } 25 | } 26 | }, 27 | }; 28 | }; 29 | 30 | export default { 31 | create, 32 | schema, 33 | }; 34 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateFunctions.js: -------------------------------------------------------------------------------- 1 | import { 2 | iterateFunctionNodes, 3 | } from '../../utilities'; 4 | import evaluateReturnType from './evaluateReturnType'; 5 | import evaluateTypical from './evaluateTypical'; 6 | 7 | export default iterateFunctionNodes((context, report) => { 8 | const checkParam = evaluateTypical(context, report, 'parameter'); 9 | const checkReturnType = evaluateReturnType(context, report); 10 | 11 | return (functionNode) => { 12 | for (const param of functionNode.params) { 13 | checkParam(param); 14 | } 15 | 16 | checkReturnType(functionNode); 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateObjectTypeIndexer.js: -------------------------------------------------------------------------------- 1 | import { 2 | getTokenAfterParens, getTokenBeforeParens, 3 | } from '../../utilities'; 4 | 5 | export default (context, report) => { 6 | const sourceCode = context.getSourceCode(); 7 | 8 | return (objectTypeIndexer) => { 9 | // type X = { [a: b]: c } 10 | // ^ 11 | report({ 12 | colon: getTokenBeforeParens(sourceCode, objectTypeIndexer.key), 13 | node: objectTypeIndexer, 14 | }); 15 | 16 | // type X = { [a: b]: c } 17 | // ^ 18 | report({ 19 | colon: sourceCode.getTokenAfter(getTokenAfterParens(sourceCode, objectTypeIndexer.key)), 20 | node: objectTypeIndexer, 21 | }); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateObjectTypeProperty.js: -------------------------------------------------------------------------------- 1 | import { 2 | getParameterName, quoteName, 3 | } from '../../utilities'; 4 | 5 | const getColon = (context, objectTypeProperty) => { 6 | let tokenIndex = 1; 7 | 8 | if (objectTypeProperty.optional) { 9 | tokenIndex++; 10 | } 11 | 12 | if (objectTypeProperty.static) { 13 | tokenIndex++; 14 | } 15 | 16 | if (objectTypeProperty.variance) { 17 | tokenIndex++; 18 | } 19 | 20 | return context.getSourceCode().getFirstToken(objectTypeProperty, tokenIndex); 21 | }; 22 | 23 | // 1) type X = { foo(): A; } 24 | // 2) type X = { foo: () => A; } 25 | // the above have identical ASTs (save for their ranges) 26 | // case 1 doesn't have a type annotation colon and must be ignored 27 | const isShortPropertyFunction = (objectTypeProperty) => { 28 | return objectTypeProperty.value.type === 'FunctionTypeAnnotation' && objectTypeProperty.range[0] === objectTypeProperty.value.range[0]; 29 | }; 30 | 31 | export default (context, report) => { 32 | return (objectTypeProperty) => { 33 | if (isShortPropertyFunction(objectTypeProperty)) { 34 | // potential difference: not checked in before 35 | return; 36 | } 37 | 38 | report({ 39 | colon: getColon(context, objectTypeProperty), 40 | name: quoteName(getParameterName(objectTypeProperty, context)), 41 | node: objectTypeProperty, 42 | }); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateReturnType.js: -------------------------------------------------------------------------------- 1 | export default (context, report) => { 2 | const sourceCode = context.getSourceCode(); 3 | 4 | return (functionNode) => { 5 | // skip FunctionTypeAnnotation, possibly another rule as it's an arrow, not a colon? 6 | // (foo: number) => string 7 | // ^^^^ 8 | if (functionNode.returnType && functionNode.type !== 'FunctionTypeAnnotation') { 9 | report({ 10 | colon: sourceCode.getFirstToken(functionNode.returnType), 11 | node: functionNode, 12 | type: 'return type', 13 | }); 14 | } 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateTypeCastExpression.js: -------------------------------------------------------------------------------- 1 | export default (context, report) => { 2 | const sourceCode = context.getSourceCode(); 3 | 4 | return (typeCastExpression) => { 5 | report({ 6 | colon: sourceCode.getFirstToken(typeCastExpression.typeAnnotation), 7 | node: typeCastExpression, 8 | type: 'type cast', 9 | }); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateTypical.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | getParameterName, quoteName, 4 | } from '../../utilities'; 5 | 6 | export default (context, report, typeForMessage) => { 7 | const sourceCode = context.getSourceCode(); 8 | 9 | const getColon = (node, typeAnnotation) => { 10 | if (node.type === 'FunctionTypeParam') { 11 | return sourceCode.getFirstToken(node, node.optional ? 2 : 1); 12 | } 13 | 14 | return sourceCode.getFirstToken(typeAnnotation); 15 | }; 16 | 17 | return (node) => { 18 | const typeAnnotation = _.get(node, 'typeAnnotation') || _.get(node, 'left.typeAnnotation'); 19 | 20 | if (typeAnnotation) { 21 | report({ 22 | colon: getColon(node, typeAnnotation), 23 | name: quoteName(getParameterName(node, context)), 24 | node, 25 | type: typeForMessage + ' type annotation', 26 | }); 27 | } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/evaluateVariables.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | getParameterName, quoteName, 4 | } from '../../utilities'; 5 | 6 | export default (context, report) => { 7 | const sourceCode = context.getSourceCode(); 8 | 9 | return (node) => { 10 | const declarations = _.get(node, 'declarations', []); 11 | 12 | for (const leaf of declarations) { 13 | const typeAnnotation = _.get(leaf, 'id.typeAnnotation'); 14 | 15 | if (typeAnnotation) { 16 | report({ 17 | colon: sourceCode.getFirstToken(typeAnnotation), 18 | name: quoteName(getParameterName(leaf, context)), 19 | node: leaf, 20 | type: node.kind + ' type annotation', 21 | }); 22 | } 23 | } 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/index.js: -------------------------------------------------------------------------------- 1 | import evaluateFunctions from './evaluateFunctions'; 2 | import evaluateObjectTypeIndexer from './evaluateObjectTypeIndexer'; 3 | import evaluateObjectTypeProperty from './evaluateObjectTypeProperty'; 4 | import evaluateTypeCastExpression from './evaluateTypeCastExpression'; 5 | import evaluateTypical from './evaluateTypical'; 6 | import evaluateVariables from './evaluateVariables'; 7 | import reporter from './reporter'; 8 | 9 | export default (direction, context, options) => { 10 | const report = reporter(direction, context, options); 11 | 12 | return { 13 | ...evaluateFunctions(context, report), 14 | ClassProperty: evaluateTypical(context, report, 'class property'), 15 | ObjectTypeIndexer: evaluateObjectTypeIndexer(context, report), 16 | ObjectTypeProperty: evaluateObjectTypeProperty(context, report), 17 | TypeCastExpression: evaluateTypeCastExpression(context, report), 18 | VariableDeclaration: evaluateVariables(context, report), 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/rules/typeColonSpacing/reporter.js: -------------------------------------------------------------------------------- 1 | import { 2 | spacingFixers, 3 | } from '../../utilities'; 4 | 5 | const hasLineBreak = (direction, colon, context) => { 6 | const sourceCode = context.getSourceCode(); 7 | 8 | if (direction === 'before') { 9 | return colon.loc.start.line !== sourceCode.getTokenBefore(colon).loc.end.line; 10 | } 11 | 12 | return sourceCode.getTokenAfter(colon).loc.start.line !== colon.loc.end.line; 13 | }; 14 | 15 | const getSpaces = (direction, colon, context) => { 16 | const sourceCode = context.getSourceCode(); 17 | 18 | if (direction === 'before') { 19 | return colon.range[0] - sourceCode.getTokenBefore(colon).range[1]; 20 | } 21 | 22 | return sourceCode.getTokenAfter(colon).range[0] - colon.range[1]; 23 | }; 24 | 25 | export default (direction, context, {always, allowLineBreak}) => { 26 | return ({colon, node, name = '', type = 'type annotation'}) => { 27 | let lineBreak; 28 | let spaces; 29 | 30 | // Support optional names 31 | // type X = { [string]: a } 32 | // type X = string => string 33 | if (!colon || colon.value !== ':') { 34 | return; 35 | } 36 | 37 | const data = { 38 | direction, 39 | name, 40 | type, 41 | }; 42 | 43 | if (hasLineBreak(direction, colon, context)) { 44 | if (allowLineBreak) { 45 | spaces = 1; 46 | } else { 47 | lineBreak = true; 48 | spaces = getSpaces(direction, colon, context); 49 | } 50 | } else { 51 | spaces = getSpaces(direction, colon, context); 52 | } 53 | 54 | if (always && lineBreak) { 55 | context.report({ 56 | data, 57 | fix: spacingFixers.replaceWithSpace(direction, colon, spaces), 58 | message: 'There must not be a line break {{direction}} {{name}}{{type}} colon.', 59 | node, 60 | }); 61 | } else if (always && spaces > 1) { 62 | context.report({ 63 | data, 64 | fix: spacingFixers.stripSpaces(direction, colon, spaces - 1), 65 | message: 'There must be 1 space {{direction}} {{name}}{{type}} colon.', 66 | node, 67 | }); 68 | } else if (always && spaces === 0) { 69 | context.report({ 70 | data, 71 | fix: spacingFixers.addSpace(direction, colon), 72 | message: 'There must be a space {{direction}} {{name}}{{type}} colon.', 73 | node, 74 | }); 75 | } else if (!always && spaces > 0) { 76 | context.report({ 77 | data, 78 | fix: spacingFixers.stripSpaces(direction, colon, spaces), 79 | message: 'There must be no space {{direction}} {{name}}{{type}} colon.', 80 | node, 81 | }); 82 | } 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /src/rules/typeIdMatch.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | type: 'string', 4 | }, 5 | ]; 6 | 7 | const create = (context) => { 8 | const pattern = new RegExp(context.options[0] || '^([A-Z][a-z0-9]*)+Type$', 'u'); 9 | 10 | const checkType = (typeAliasNode) => { 11 | const typeIdentifierName = typeAliasNode.id.name; 12 | 13 | if (!pattern.test(typeIdentifierName)) { 14 | context.report({data: { 15 | name: typeIdentifierName, 16 | pattern: pattern.toString(), 17 | }, 18 | message: 'Type identifier \'{{name}}\' does not match pattern \'{{pattern}}\'.', 19 | node: typeAliasNode}); 20 | } 21 | }; 22 | 23 | return { 24 | OpaqueType: checkType, 25 | TypeAlias: checkType, 26 | }; 27 | }; 28 | 29 | export default { 30 | create, 31 | schema, 32 | }; 33 | -------------------------------------------------------------------------------- /src/rules/typeImportStyle.js: -------------------------------------------------------------------------------- 1 | const schema = [ 2 | { 3 | enum: ['declaration', 'identifier'], 4 | type: 'string', 5 | }, 6 | { 7 | additionalProperties: false, 8 | properties: { 9 | ignoreTypeDefault: { 10 | type: 'boolean', 11 | }, 12 | }, 13 | type: 'object', 14 | }, 15 | ]; 16 | 17 | const create = (context) => { 18 | if (context.options[0] === 'declaration') { 19 | return { 20 | ImportDeclaration (node) { 21 | if (node.importKind !== 'type') { 22 | for (const specifier of node.specifiers) { 23 | if (specifier.importKind === 'type') { 24 | context.report({ 25 | message: 'Unexpected type import', 26 | node, 27 | }); 28 | } 29 | } 30 | } 31 | }, 32 | }; 33 | } 34 | 35 | // Default to 'identifier' 36 | const ignoreTypeDefault = context.options[1] && 37 | context.options[1].ignoreTypeDefault; 38 | let isInsideDeclareModule = false; 39 | 40 | return { 41 | DeclareModule () { 42 | isInsideDeclareModule = true; 43 | }, 44 | 'DeclareModule:exit' () { 45 | isInsideDeclareModule = false; 46 | }, 47 | ImportDeclaration (node) { 48 | if (node.importKind !== 'type') { 49 | return; 50 | } 51 | 52 | // type specifiers are not allowed inside module declarations: 53 | // https://github.com/facebook/flow/issues/7609 54 | if (isInsideDeclareModule) { 55 | return; 56 | } 57 | 58 | if ( 59 | ignoreTypeDefault && 60 | node.specifiers[0] && 61 | node.specifiers[0].type === 'ImportDefaultSpecifier' 62 | ) { 63 | return; 64 | } 65 | 66 | context.report({ 67 | fix (fixer) { 68 | const imports = node.specifiers.map((specifier) => { 69 | if (specifier.type === 'ImportDefaultSpecifier') { 70 | return 'type default as ' + specifier.local.name; 71 | } 72 | 73 | if (specifier.imported.name === specifier.local.name) { 74 | return 'type ' + specifier.local.name; 75 | } 76 | 77 | return 'type ' + specifier.imported.name + ' as ' + specifier.local.name; 78 | }); 79 | const source = node.source.value; 80 | 81 | return fixer.replaceText(node, 'import {' + imports.join(', ') + '} from \'' + source + '\';'); 82 | }, 83 | message: 'Unexpected "import type"', 84 | node, 85 | }); 86 | }, 87 | }; 88 | }; 89 | 90 | export default { 91 | create, 92 | meta: { 93 | fixable: 'code', 94 | }, 95 | schema, 96 | }; 97 | -------------------------------------------------------------------------------- /src/rules/unionIntersectionSpacing.js: -------------------------------------------------------------------------------- 1 | import { 2 | spacingFixers, getTokenAfterParens, 3 | } from '../utilities'; 4 | 5 | const schema = [ 6 | { 7 | enum: ['always', 'never'], 8 | type: 'string', 9 | }, 10 | ]; 11 | 12 | const create = (context) => { 13 | const sourceCode = context.getSourceCode(); 14 | 15 | const always = (context.options[0] || 'always') === 'always'; 16 | 17 | const check = (node) => { 18 | for (const [index, type] of node.types.entries()) { 19 | if (index + 1 === node.types.length) { 20 | continue; 21 | } 22 | 23 | const separator = getTokenAfterParens(sourceCode, type); 24 | const endOfType = sourceCode.getTokenBefore(separator); 25 | const nextType = sourceCode.getTokenAfter(separator); 26 | 27 | const spaceBefore = separator.range[0] - endOfType.range[1]; 28 | const spaceAfter = nextType.range[0] - separator.range[1]; 29 | 30 | const data = {type: node.type === 'UnionTypeAnnotation' ? 'union' : 'intersection'}; 31 | 32 | if (always) { 33 | if (!spaceBefore) { 34 | context.report({ 35 | data, 36 | fix: spacingFixers.addSpaceAfter(endOfType), 37 | message: 'There must be a space before {{type}} type annotation separator', 38 | node, 39 | }); 40 | } 41 | 42 | if (!spaceAfter) { 43 | context.report({ 44 | data, 45 | fix: spacingFixers.addSpaceAfter(separator), 46 | message: 'There must be a space after {{type}} type annotation separator', 47 | node, 48 | }); 49 | } 50 | } else { 51 | if (spaceBefore) { 52 | context.report({ 53 | data, 54 | fix: spacingFixers.stripSpacesAfter(endOfType, spaceBefore), 55 | message: 'There must be no space before {{type}} type annotation separator', 56 | node, 57 | }); 58 | } 59 | 60 | if (spaceAfter) { 61 | context.report({ 62 | data, 63 | fix: spacingFixers.stripSpacesAfter(separator, spaceAfter), 64 | message: 'There must be no space after {{type}} type annotation separator', 65 | node, 66 | }); 67 | } 68 | } 69 | } 70 | }; 71 | 72 | return { 73 | IntersectionTypeAnnotation: check, 74 | UnionTypeAnnotation: check, 75 | }; 76 | }; 77 | 78 | export default { 79 | create, 80 | meta: { 81 | fixable: 'code', 82 | }, 83 | schema, 84 | }; 85 | -------------------------------------------------------------------------------- /src/rules/useFlowType.js: -------------------------------------------------------------------------------- 1 | const schema = []; 2 | 3 | const create = (context) => { 4 | const markTypeAsUsed = (node) => { 5 | context.markVariableAsUsed(node.id.name); 6 | }; 7 | 8 | const markTypeAsUsedWithGenericType = (node) => { 9 | let typeId; 10 | let scope; 11 | let variable; 12 | 13 | if (node.id.type === 'Identifier') { 14 | typeId = node.id; 15 | } else if (node.id.type === 'QualifiedTypeIdentifier') { 16 | typeId = node.id; 17 | do { 18 | typeId = typeId.qualification; 19 | } while (typeId.qualification); 20 | } 21 | 22 | for (scope = context.getScope(); scope; scope = scope.upper) { 23 | variable = scope.set.get(typeId.name); 24 | if (variable && variable.defs.length) { 25 | context.markVariableAsUsed(typeId.name); 26 | break; 27 | } 28 | } 29 | }; 30 | 31 | return { 32 | DeclareClass: markTypeAsUsed, 33 | DeclareFunction: markTypeAsUsed, 34 | DeclareModule: markTypeAsUsed, 35 | DeclareVariable: markTypeAsUsed, 36 | GenericTypeAnnotation: markTypeAsUsedWithGenericType, 37 | TypeParameterDeclaration (node) { 38 | for (const param of node.params) { 39 | if (param.default && param.default.typeParameters) { 40 | if (param.default.type === 'GenericTypeAnnotation') { 41 | markTypeAsUsedWithGenericType(param.default); 42 | } 43 | 44 | for (const typeParameterNode of param.default.typeParameters.params) { 45 | if (typeParameterNode.type === 'GenericTypeAnnotation') { 46 | markTypeAsUsedWithGenericType(typeParameterNode); 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | }; 53 | }; 54 | 55 | export default { 56 | create, 57 | schema, 58 | }; 59 | -------------------------------------------------------------------------------- /src/rules/useReadOnlySpread.js: -------------------------------------------------------------------------------- 1 | const meta = { 2 | messages: { 3 | readonlySpread: 4 | 'Flow type with spread property and all readonly properties must be ' + 5 | 'wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.', 6 | }, 7 | }; 8 | 9 | const create = (context) => { 10 | return { 11 | TypeAlias (node) { 12 | if (node.right.type === 'GenericTypeAnnotation' && node.right.id.name === '$ReadOnly') { 13 | // it's already $ReadOnly<…>, nothing to do 14 | } else if (node.right.type === 'ObjectTypeAnnotation') { 15 | // let's iterate all props and if everything is readonly then throw 16 | let shouldThrow = false; 17 | let hasSpread = false; 18 | for (const property of node.right.properties) { 19 | if (property.type === 'ObjectTypeProperty') { 20 | if (property.variance && property.variance.kind === 'plus') { 21 | shouldThrow = true; 22 | } else { 23 | shouldThrow = false; 24 | break; 25 | } 26 | } else if (property.type === 'ObjectTypeSpreadProperty') { 27 | hasSpread = true; 28 | } 29 | } 30 | 31 | if (hasSpread === true && shouldThrow === true) { 32 | context.report({ 33 | messageId: 'readonlySpread', 34 | node: node.right, 35 | }); 36 | } 37 | } 38 | }, 39 | }; 40 | }; 41 | 42 | export default { 43 | create, 44 | meta, 45 | }; 46 | -------------------------------------------------------------------------------- /src/rules/validSyntax.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | getParameterName, 4 | iterateFunctionNodes, 5 | quoteName, 6 | } from '../utilities'; 7 | 8 | const schema = []; 9 | 10 | const create = iterateFunctionNodes((context) => { 11 | return (functionNode) => { 12 | for (const identifierNode of functionNode.params) { 13 | const nodeType = _.get(identifierNode, 'type'); 14 | const isAssignmentPattern = nodeType === 'AssignmentPattern'; 15 | const hasTypeAnnotation = Boolean(_.get(identifierNode, 'typeAnnotation')); 16 | const leftAnnotated = Boolean(_.get(identifierNode, 'left.typeAnnotation')); 17 | 18 | if (isAssignmentPattern && hasTypeAnnotation && !leftAnnotated) { 19 | context.report({ 20 | data: { 21 | name: quoteName(getParameterName(identifierNode, context)), 22 | }, 23 | message: '{{name}}parameter type annotation must be placed on left-hand side of assignment.', 24 | node: identifierNode, 25 | }); 26 | } 27 | } 28 | }; 29 | }); 30 | 31 | export default { 32 | create, 33 | meta: { 34 | deprecated: true, 35 | }, 36 | schema, 37 | }; 38 | -------------------------------------------------------------------------------- /src/utilities/checkFlowFileAnnotation.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import isFlowFile from './isFlowFile'; 3 | import isNoFlowFile from './isNoFlowFile'; 4 | 5 | export default (cb, context) => { 6 | const checkThisFile = (!_.get(context, 'settings.flowtype.onlyFilesWithFlowAnnotation') && !isNoFlowFile(context)) || isFlowFile(context); // eslint-disable-line no-extra-parens, max-len 7 | 8 | if (!checkThisFile) { 9 | return () => {}; 10 | } 11 | 12 | // eslint-disable-next-line promise/prefer-await-to-callbacks -- not a promise callback 13 | return cb(context); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utilities/fuzzyStringMatch.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | // Creates an array of letter pairs from a given array 4 | // origin: https://github.com/d3/d3-array/blob/master/src/pairs.js 5 | const arrayPairs = (array) => { 6 | let ii = 0; 7 | const length = array.length - 1; 8 | let letter = array[0]; 9 | const pairs = Array.from({length: length < 0 ? 0 : length}); 10 | 11 | while (ii < length) { 12 | pairs[ii] = [letter, letter = array[++ii]]; 13 | } 14 | 15 | return pairs; 16 | }; 17 | 18 | // Based on http://stackoverflow.com/a/23305385 19 | 20 | const stringSimilarity = (str1, str2) => { 21 | if (str1.length > 0 && str2.length > 0) { 22 | const pairs1 = arrayPairs(str1); 23 | const pairs2 = arrayPairs(str2); 24 | const unionLen = pairs1.length + pairs2.length; 25 | let hitCount; 26 | 27 | hitCount = 0; 28 | 29 | _.forIn(pairs1, (val1) => { 30 | _.forIn(pairs2, (val2) => { 31 | if (_.isEqual(val1, val2)) { 32 | hitCount++; 33 | } 34 | }); 35 | }); 36 | 37 | if (hitCount > 0) { 38 | return 2 * hitCount / unionLen; 39 | } 40 | } 41 | 42 | return 0; 43 | }; 44 | 45 | export default (needle, haystack, weight = 0.5) => { 46 | return stringSimilarity(needle, haystack) >= Number(weight); 47 | }; 48 | -------------------------------------------------------------------------------- /src/utilities/getBuiltinRule.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require */ 2 | 3 | /** 4 | * Adopted from https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/utils/get-builtin-rule.js. 5 | */ 6 | export const getBuiltinRule = (id) => { 7 | // TODO: Remove this when we drop support for ESLint 7 8 | const eslintVersion = require('eslint/package.json').version; 9 | /* istanbul ignore next */ 10 | if (eslintVersion.startsWith('7.')) { 11 | return require(`eslint/lib/rules/${id}`); 12 | } 13 | 14 | return require('eslint/use-at-your-own-risk').builtinRules.get(id); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utilities/getParameterName.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default (identifierNode, context) => { 4 | if (_.has(identifierNode, 'name')) { 5 | return identifierNode.name; 6 | } 7 | 8 | if (_.has(identifierNode, 'left.name')) { 9 | return identifierNode.left.name; 10 | } 11 | 12 | if (_.has(identifierNode, 'key.name')) { 13 | return identifierNode.key.name; 14 | } 15 | 16 | if (identifierNode.type === 'RestElement') { 17 | return identifierNode.argument.name; 18 | } 19 | 20 | if (identifierNode.type === 'ObjectTypeProperty') { 21 | let tokenIndex; 22 | 23 | tokenIndex = 0; 24 | 25 | if (identifierNode.static) { 26 | tokenIndex++; 27 | } 28 | 29 | if (identifierNode.variance) { 30 | tokenIndex++; 31 | } 32 | 33 | if (identifierNode.kind === 'set' || identifierNode.kind === 'get') { 34 | tokenIndex++; 35 | } 36 | 37 | return context.getSourceCode().getFirstToken(identifierNode, tokenIndex).value; 38 | } 39 | 40 | if (identifierNode.type === 'ObjectTypeIndexer') { 41 | let tokenIndex; 42 | 43 | tokenIndex = 0; 44 | 45 | if (identifierNode.static) { 46 | tokenIndex++; 47 | } 48 | 49 | if (identifierNode.variance) { 50 | tokenIndex++; 51 | } 52 | 53 | tokenIndex++; 54 | 55 | const id = context.getSourceCode().getFirstToken(identifierNode, tokenIndex); 56 | const colonOrBrace = context.getSourceCode().getTokenAfter(id); 57 | if (colonOrBrace.value === ':') { 58 | return id.value; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | if (identifierNode.type === 'FunctionTypeParam') { 65 | return context.getSourceCode().getFirstToken(identifierNode).value; 66 | } 67 | 68 | if (identifierNode.type === 'ObjectPattern' || identifierNode.type === 'ArrayPattern') { 69 | const text = context.getSourceCode().getText(identifierNode); 70 | 71 | if (identifierNode.typeAnnotation) { 72 | return text.replace(context.getSourceCode().getText(identifierNode.typeAnnotation), '').trim(); 73 | } 74 | 75 | return text; 76 | } 77 | 78 | if (_.get(identifierNode, 'left.type') === 'ObjectPattern') { 79 | return context.getSourceCode().getText(identifierNode.left); 80 | } 81 | 82 | return null; 83 | }; 84 | -------------------------------------------------------------------------------- /src/utilities/getTokenAfterParens.js: -------------------------------------------------------------------------------- 1 | const getTokenAfterParens = (sourceCode, node) => { 2 | let token; 3 | 4 | token = sourceCode.getTokenAfter(node); 5 | 6 | while (token.type === 'Punctuator' && token.value === ')') { 7 | token = sourceCode.getTokenAfter(token); 8 | } 9 | 10 | return token; 11 | }; 12 | 13 | export default getTokenAfterParens; 14 | -------------------------------------------------------------------------------- /src/utilities/getTokenBeforeParens.js: -------------------------------------------------------------------------------- 1 | const getTokenBeforeParens = (sourceCode, node) => { 2 | let token; 3 | 4 | token = sourceCode.getTokenBefore(node); 5 | 6 | while (token.type === 'Punctuator' && token.value === '(') { 7 | token = sourceCode.getTokenBefore(token); 8 | } 9 | 10 | return token; 11 | }; 12 | 13 | export default getTokenBeforeParens; 14 | -------------------------------------------------------------------------------- /src/utilities/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // eslint-disable-next-line import/no-namespace 4 | import * as spacingFixers from './spacingFixers'; 5 | 6 | export { 7 | default as checkFlowFileAnnotation, 8 | } from './checkFlowFileAnnotation'; 9 | export { 10 | default as fuzzyStringMatch, 11 | } from './fuzzyStringMatch'; 12 | export { 13 | default as getParameterName, 14 | } from './getParameterName'; 15 | export { 16 | default as getTokenAfterParens, 17 | } from './getTokenAfterParens'; 18 | export { 19 | default as getTokenBeforeParens, 20 | } from './getTokenBeforeParens'; 21 | export { 22 | default as isFlowFile, 23 | } from './isFlowFile'; 24 | export { 25 | default as isNoFlowFile, 26 | } from './isNoFlowFile'; 27 | export { 28 | default as isFlowFileAnnotation, 29 | } from './isFlowFileAnnotation'; 30 | export { 31 | default as iterateFunctionNodes, 32 | } from './iterateFunctionNodes'; 33 | export { 34 | default as quoteName, 35 | } from './quoteName'; 36 | 37 | export { 38 | spacingFixers, 39 | }; 40 | -------------------------------------------------------------------------------- /src/utilities/isFlowFile.js: -------------------------------------------------------------------------------- 1 | import isFlowFileAnnotation from './isFlowFileAnnotation'; 2 | 3 | /** 4 | * Checks whether a file has an @flow or @noflow annotation. 5 | * 6 | * @param context 7 | * @param [strict] - By default, the function returns true if the file starts with @flow but not if it 8 | * starts by @noflow. When the strict flag is set to false, the function returns true if the flag has @noflow also. 9 | */ 10 | 11 | export default (context, strict = true) => { 12 | const comments = context.getAllComments(); 13 | 14 | if (!comments.length) { 15 | return false; 16 | } 17 | 18 | return comments.some((comment) => { 19 | return isFlowFileAnnotation(comment.value, strict); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/utilities/isFlowFileAnnotation.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const FLOW_MATCHER = /^@(?:no)?flow$/u; 4 | 5 | export default (comment, strict) => { 6 | // The flow parser splits comments with the following regex to look for the @flow flag. 7 | // See https://github.com/facebook/flow/blob/a96249b93541f2f7bfebd8d62085bf7a75de02f2/src/parsing/docblock.ml#L39 8 | return _.some(comment.split(/[\t\n\r */\\]+/u), (commentPart) => { 9 | const match = commentPart.match(FLOW_MATCHER); 10 | 11 | if (match === null) { 12 | return false; 13 | } 14 | 15 | return !strict || match[0] === '@flow'; 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/utilities/isNoFlowFile.js: -------------------------------------------------------------------------------- 1 | import isNoFlowFileAnnotation from './isNoFlowFileAnnotation'; 2 | 3 | /** 4 | * Checks whether a file has an @flow or @noflow annotation. 5 | * 6 | * @param context 7 | * @param [strict] - By default, the function returns true if the file starts with @flow but not if it 8 | * starts by @noflow. When the strict flag is set to false, the function returns true if the flag has @noflow also. 9 | */ 10 | 11 | export default (context, strict = true) => { 12 | const comments = context.getAllComments(); 13 | 14 | if (!comments.length) { 15 | return false; 16 | } 17 | 18 | return comments.some((comment) => { 19 | return isNoFlowFileAnnotation(comment.value, strict); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/utilities/isNoFlowFileAnnotation.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const FLOW_MATCHER = /^@noflow$/u; 4 | 5 | export default (comment, strict) => { 6 | // The flow parser splits comments with the following regex to look for the @flow flag. 7 | // See https://github.com/facebook/flow/blob/a96249b93541f2f7bfebd8d62085bf7a75de02f2/src/parsing/docblock.ml#L39 8 | return _.some(comment.split(/[\t\n\r */\\]+/u), (commentPart) => { 9 | const match = commentPart.match(FLOW_MATCHER); 10 | 11 | if (match === null) { 12 | return false; 13 | } 14 | 15 | return !strict || match[0] === '@noflow'; 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/utilities/iterateFunctionNodes.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line eslint-plugin/prefer-object-rule -- false positive, this is not a rule 2 | export default (iterator) => { 3 | return (context, ...rest) => { 4 | const nodeIterator = iterator(context, ...rest); 5 | 6 | return { 7 | ArrowFunctionExpression: nodeIterator, 8 | FunctionDeclaration: nodeIterator, 9 | FunctionExpression: nodeIterator, 10 | FunctionTypeAnnotation: nodeIterator, 11 | }; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utilities/quoteName.js: -------------------------------------------------------------------------------- 1 | export default (name) => { 2 | return name ? '"' + name + '" ' : ''; 3 | }; 4 | -------------------------------------------------------------------------------- /src/utilities/spacingFixers.js: -------------------------------------------------------------------------------- 1 | export const stripSpacesBefore = (node, spaces) => { 2 | return (fixer) => { 3 | return fixer.removeRange([node.range[0] - spaces, node.range[0]]); 4 | }; 5 | }; 6 | 7 | export const stripSpacesAfter = (node, spaces) => { 8 | return (fixer) => { 9 | return fixer.removeRange([node.range[1], node.range[1] + spaces]); 10 | }; 11 | }; 12 | 13 | export const addSpaceBefore = (node) => { 14 | return (fixer) => { 15 | return fixer.insertTextBefore(node, ' '); 16 | }; 17 | }; 18 | 19 | export const addSpaceAfter = (node) => { 20 | return (fixer) => { 21 | return fixer.insertTextAfter(node, ' '); 22 | }; 23 | }; 24 | 25 | export const replaceWithSpaceBefore = (node, spaces) => { 26 | return (fixer) => { 27 | return fixer.replaceTextRange([node.range[0] - spaces, node.range[0]], ' '); 28 | }; 29 | }; 30 | 31 | export const replaceWithSpaceAfter = (node, spaces) => { 32 | return (fixer) => { 33 | return fixer.replaceTextRange([node.range[1], node.range[1] + spaces], ' '); 34 | }; 35 | }; 36 | 37 | export const stripSpaces = (direction, node, spaces) => { 38 | if (direction === 'before') { 39 | return stripSpacesBefore(node, spaces); 40 | } 41 | 42 | return stripSpacesAfter(node, spaces); 43 | }; 44 | 45 | export const addSpace = (direction, node) => { 46 | if (direction === 'before') { 47 | return addSpaceBefore(node); 48 | } 49 | 50 | return addSpaceAfter(node); 51 | }; 52 | 53 | export const replaceWithSpace = (direction, node, spaces) => { 54 | if (direction === 'before') { 55 | return replaceWithSpaceBefore(node, spaces); 56 | } 57 | 58 | return replaceWithSpaceAfter(node, spaces); 59 | }; 60 | -------------------------------------------------------------------------------- /tests/rules/assertions/arrayStyleComplexType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type X = (?string)[]', 5 | errors: [{message: 'Use "Array", not "(?string)[]"'}], 6 | output: 'type X = Array', 7 | }, 8 | { 9 | code: 'type X = (?string)[]', 10 | errors: [{message: 'Use "Array", not "(?string)[]"'}], 11 | options: ['verbose'], 12 | output: 'type X = Array', 13 | }, 14 | { 15 | code: 'type X = Array', 16 | errors: [{message: 'Use "(?string)[]", not "Array"'}], 17 | options: ['shorthand'], 18 | output: 'type X = (?string)[]', 19 | }, 20 | { 21 | code: 'type X = Array<{foo: string}>', 22 | errors: [{message: 'Use "{foo: string}[]", not "Array<{foo: string}>"'}], 23 | options: ['shorthand'], 24 | output: 'type X = {foo: string}[]', 25 | }, 26 | { 27 | code: 'type X = (string | number)[]', 28 | errors: [{message: 'Use "Array", not "(string | number)[]"'}], 29 | output: 'type X = Array', 30 | }, 31 | { 32 | code: 'type X = (string & number)[]', 33 | errors: [{message: 'Use "Array", not "(string & number)[]"'}], 34 | output: 'type X = Array', 35 | }, 36 | { 37 | code: 'type X = [string, number][]', 38 | errors: [{message: 'Use "Array<[string, number]>", not "[string, number][]"'}], 39 | output: 'type X = Array<[string, number]>', 40 | }, 41 | { 42 | code: 'type X = {foo: string}[]', 43 | errors: [{message: 'Use "Array<{foo: string}>", not "{foo: string}[]"'}], 44 | output: 'type X = Array<{foo: string}>', 45 | }, 46 | { 47 | code: 'type X = (string => number)[]', 48 | errors: [{message: 'Use "Array number>", not "(string => number)[]"'}], 49 | output: 'type X = Array number>', 50 | }, 51 | { 52 | code: 'type X = {\n foo: string,\n bar: number\n}[]', 53 | errors: [{message: 'Use "Array<{ foo: string, bar: number }>", not "{ foo: string, bar: number }[]"'}], 54 | output: 'type X = Array<{\n foo: string,\n bar: number\n}>', 55 | }, 56 | { 57 | code: 'type X = {\n foo: string,\n bar: number,\n quo: boolean,\n hey: Date\n}[]', 58 | errors: [{message: 'Use "Array", not "Type[]"'}], 59 | output: 'type X = Array<{\n foo: string,\n bar: number,\n quo: boolean,\n hey: Date\n}>', 60 | }, 61 | ], 62 | misconfigured: [ 63 | { 64 | errors: [ 65 | { 66 | data: 'normal', 67 | instancePath: '/0', 68 | keyword: 'enum', 69 | message: 'must be equal to one of the allowed values', 70 | params: { 71 | allowedValues: [ 72 | 'verbose', 73 | 'shorthand', 74 | ], 75 | }, 76 | parentSchema: { 77 | enum: [ 78 | 'verbose', 79 | 'shorthand', 80 | ], 81 | type: 'string', 82 | }, 83 | schema: [ 84 | 'verbose', 85 | 'shorthand', 86 | ], 87 | schemaPath: '#/items/0/enum', 88 | }, 89 | ], 90 | options: ['normal'], 91 | }, 92 | ], 93 | valid: [ 94 | { 95 | code: 'type X = Array', 96 | }, 97 | { 98 | code: 'type X = Array', 99 | options: ['verbose'], 100 | }, 101 | { 102 | code: 'type X = (?string)[]', 103 | options: ['shorthand'], 104 | }, 105 | { 106 | code: 'type X = Array', 107 | options: ['shorthand'], 108 | }, 109 | { 110 | code: 'type X = Array', 111 | options: ['shorthand'], 112 | settings: { 113 | flowtype: { 114 | onlyFilesWithFlowAnnotation: true, 115 | }, 116 | }, 117 | }, 118 | ], 119 | }; 120 | -------------------------------------------------------------------------------- /tests/rules/assertions/booleanStyle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type X = bool', 5 | errors: [{message: 'Use "boolean", not "bool"'}], 6 | output: 'type X = boolean', 7 | }, 8 | { 9 | code: 'type X = bool', 10 | errors: [{message: 'Use "boolean", not "bool"'}], 11 | options: ['boolean'], 12 | output: 'type X = boolean', 13 | }, 14 | { 15 | code: 'type X = boolean', 16 | errors: [{message: 'Use "bool", not "boolean"'}], 17 | options: ['bool'], 18 | output: 'type X = bool', 19 | }, 20 | ], 21 | misconfigured: [ 22 | { 23 | errors: [ 24 | { 25 | data: 'integer', 26 | instancePath: '/0', 27 | keyword: 'enum', 28 | message: 'must be equal to one of the allowed values', 29 | params: { 30 | allowedValues: [ 31 | 'bool', 32 | 'boolean', 33 | ], 34 | }, 35 | parentSchema: { 36 | enum: [ 37 | 'bool', 38 | 'boolean', 39 | ], 40 | type: 'string', 41 | }, 42 | schema: [ 43 | 'bool', 44 | 'boolean', 45 | ], 46 | schemaPath: '#/items/0/enum', 47 | }, 48 | ], 49 | options: ['integer'], 50 | }, 51 | ], 52 | valid: [ 53 | { 54 | code: 'type X = boolean', 55 | }, 56 | { 57 | code: 'type X = boolean', 58 | options: ['boolean'], 59 | }, 60 | { 61 | code: 'type X = bool', 62 | options: ['bool'], 63 | }, 64 | { 65 | code: 'type X = bool', 66 | options: ['boolean'], 67 | settings: { 68 | flowtype: { 69 | onlyFilesWithFlowAnnotation: true, 70 | }, 71 | }, 72 | }, 73 | ], 74 | }; 75 | -------------------------------------------------------------------------------- /tests/rules/assertions/enforceLineBreak.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type baz = 6;\nconst hi = 2;', 5 | errors: [{ 6 | message: 'New line required below type declaration', 7 | }], 8 | output: 'type baz = 6;\n\nconst hi = 2;', 9 | }, 10 | { 11 | code: 'const foo = 6;\ntype hi = 2;\n', 12 | errors: [ 13 | {message: 'New line required above type declaration'}, 14 | ], 15 | output: 'const foo = 6;\n\ntype hi = 2;\n', 16 | }, 17 | { 18 | code: 'const som = "jes";\n// a comment\ntype fed = "hed";\n', 19 | errors: [ 20 | {message: 'New line required above type declaration'}, 21 | ], 22 | output: 'const som = "jes";\n\n// a comment\ntype fed = "hed";\n', 23 | }, 24 | { 25 | code: 'type som = "jes";\n// a comment\nconst fed = "hed";\n', 26 | errors: [ 27 | {message: 'New line required below type declaration'}, 28 | ], 29 | output: 'type som = "jes";\n\n// a comment\nconst fed = "hed";\n', 30 | }, 31 | { 32 | code: 'type hello = 34;\nconst som = "jes";\ntype fed = "hed";\n', 33 | errors: [ 34 | {message: 'New line required below type declaration'}, 35 | {message: 'New line required above type declaration'}, 36 | ], 37 | output: 'type hello = 34;\n\nconst som = "jes";\n\ntype fed = "hed";\n', 38 | }, 39 | { 40 | code: 'const a = 5;\nexport type hello = 34;\n', 41 | errors: [ 42 | {message: 'New line required above type declaration'}, 43 | ], 44 | output: 'const a = 5;\n\nexport type hello = 34;\n', 45 | }, 46 | { 47 | code: 'const a = 5;\n// a comment\nexport type hello = 34;\n', 48 | errors: [ 49 | {message: 'New line required above type declaration'}, 50 | ], 51 | output: 'const a = 5;\n\n// a comment\nexport type hello = 34;\n', 52 | }, 53 | { 54 | code: `const a = 5; 55 | /** 56 | * a jsdoc block 57 | */ 58 | type hello = 34;`, 59 | errors: [ 60 | {message: 'New line required above type declaration'}, 61 | ], 62 | output: `const a = 5; 63 | 64 | /** 65 | * a jsdoc block 66 | */ 67 | type hello = 34;`, 68 | }, 69 | ], 70 | valid: [ 71 | { 72 | code: 'type gjs = 6;', 73 | }, 74 | { 75 | code: 'type gjs = 6;\n\ntype hi = 2;\n', 76 | }, 77 | { 78 | code: 79 | `type X = 4; 80 | 81 | const red = "serpent"; 82 | console.log("hello"); 83 | 84 | // number or string 85 | type Y = string | number; 86 | 87 | // resting + sleep 88 | type snooze = "dreaming" | "";`, 89 | }, 90 | { 91 | code: 92 | `type Props = { 93 | accountBalance: string | number, 94 | accountNumber: string | number, 95 | };`, 96 | }, 97 | { 98 | code: 99 | `const x = 4; 100 | const y = 489; 101 | 102 | // Some Comment 103 | type Props = { 104 | accountBalance: string | number, 105 | accountNumber: string | number, 106 | }; 107 | 108 | type RoadT = "grass" | "gravel" | "cement";`, 109 | }, 110 | { 111 | code: '// @flow\ntype A = string', 112 | }, 113 | ], 114 | }; 115 | -------------------------------------------------------------------------------- /tests/rules/assertions/interfaceIdMatch.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'interface foo{};', 5 | errors: [ 6 | { 7 | message: 'Interface identifier \'foo\' does not match pattern \'/^([A-Z][a-z0-9]*)+Type$/u\'.', 8 | }, 9 | ], 10 | }, 11 | { 12 | code: 'interface FooType{};', 13 | errors: [ 14 | { 15 | message: 'Interface identifier \'FooType\' does not match pattern \'/^foo$/u\'.', 16 | }, 17 | ], 18 | options: [ 19 | '^foo$', 20 | ], 21 | }, 22 | ], 23 | misconfigured: [ 24 | { 25 | errors: [ 26 | { 27 | data: 7, 28 | instancePath: '/0', 29 | keyword: 'type', 30 | message: 'must be string', 31 | params: { 32 | type: 'string', 33 | }, 34 | parentSchema: { 35 | type: 'string', 36 | }, 37 | schema: 'string', 38 | schemaPath: '#/items/0/type', 39 | }, 40 | ], 41 | options: [7], 42 | }, 43 | ], 44 | valid: [ 45 | { 46 | code: 'interface FooType {};', 47 | }, 48 | { 49 | code: 'interface foo {};', 50 | options: [ 51 | '^foo$', 52 | ], 53 | }, 54 | { 55 | code: 'interface foo {};', 56 | settings: { 57 | flowtype: { 58 | onlyFilesWithFlowAnnotation: true, 59 | }, 60 | }, 61 | }, 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /tests/rules/assertions/newlineAfterFlowAnnotation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: '// @flow\nimport Foo from \'./foo\';', 5 | errors: [{message: 'Expected newline after flow annotation'}], 6 | output: '// @flow\n\nimport Foo from \'./foo\';', 7 | }, 8 | { 9 | code: '// @flow\nimport Foo from \'./foo\';', 10 | errors: [{message: 'Expected newline after flow annotation'}], 11 | options: ['always'], 12 | output: '// @flow\n\nimport Foo from \'./foo\';', 13 | }, 14 | { 15 | code: '// @flow\r\nimport Foo from \'./foo\';', 16 | errors: [{message: 'Expected newline after flow annotation'}], 17 | options: ['always-windows'], 18 | output: '// @flow\r\n\r\nimport Foo from \'./foo\';', 19 | }, 20 | { 21 | code: '// @flow\n\n', 22 | errors: [{message: 'Expected no newline after flow annotation'}], 23 | options: ['never'], 24 | output: '// @flow\n', 25 | }, 26 | ], 27 | valid: [ 28 | { 29 | code: '// @flow\n\nimport Foo from \'./foo\';', 30 | options: ['always'], 31 | }, 32 | { 33 | code: '// @flow\r\n\r\nimport Foo from \'./foo\';', 34 | options: ['always-windows'], 35 | }, 36 | { 37 | code: '// @flow\nimport Foo from \'./foo\';', 38 | options: ['never'], 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /tests/rules/assertions/noDuplicateTypeUnionIntersectionMembers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type A = 1 | 2 | 3 | 1;', 5 | errors: [{message: 'Duplicate union member found "1".'}], 6 | output: 'type A = 1 | 2 | 3;', 7 | }, 8 | { 9 | code: 'type B = \'foo\' | \'bar\' | \'foo\';', 10 | errors: [{message: 'Duplicate union member found "\'foo\'".'}], 11 | output: 'type B = \'foo\' | \'bar\';', 12 | }, 13 | { 14 | code: 'type C = A | B | A | B;', 15 | errors: [ 16 | {message: 'Duplicate union member found "A".'}, 17 | {message: 'Duplicate union member found "B".'}, 18 | ], 19 | output: 'type C = A | B;', 20 | }, 21 | { 22 | code: 'type C = A & B & A & B;', 23 | errors: [ 24 | {message: 'Duplicate intersection member found "A".'}, 25 | {message: 'Duplicate intersection member found "B".'}, 26 | ], 27 | output: 'type C = A & B;', 28 | }, 29 | ], 30 | valid: [ 31 | { 32 | code: 'type A = 1 | 2 | 3;', 33 | }, 34 | { 35 | code: 'type B = \'foo\' | \'bar\';', 36 | }, 37 | { 38 | code: 'type C = A | B;', 39 | }, 40 | { 41 | code: 'type C = A & B;', 42 | }, 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /tests/rules/assertions/noExistentialType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type T = *;', 5 | errors: [{message: 'Unexpected use of existential type (*).'}], 6 | }, 7 | { 8 | code: 'type T = U<*, *>;', 9 | errors: [ 10 | {column: 12, 11 | message: 'Unexpected use of existential type (*).'}, 12 | {column: 15, 13 | message: 'Unexpected use of existential type (*).'}, 14 | ], 15 | }, 16 | { 17 | code: 'const f: (*) => null = () => null;', 18 | errors: [{message: 'Unexpected use of existential type (*).'}], 19 | }, 20 | ], 21 | valid: [ 22 | { 23 | code: 'type T = string | null', 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /tests/rules/assertions/noFlowFixMeComments.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: '// $FlowFixMe I am doing something evil here\nconst text = \'HELLO\';', 5 | errors: [ 6 | { 7 | message: '$FlowFixMe is treated as `any` and must be fixed.', 8 | }, 9 | ], 10 | }, 11 | { 12 | code: '// $FlowFixMe I am doing something evil here\nconst text = \'HELLO\';', 13 | errors: [ 14 | { 15 | message: '$FlowFixMe is treated as `any` and must be fixed. Fix it or match `/TODO [0-9]+/u`.', 16 | }, 17 | ], 18 | options: [ 19 | 'TODO [0-9]+', 20 | ], 21 | }, 22 | { 23 | code: '// $FlowFixMe TODO abc 47 I am doing something evil here\nconst text = \'HELLO\';', 24 | errors: [ 25 | { 26 | message: '$FlowFixMe is treated as `any` and must be fixed. Fix it or match `/TODO [0-9]+/u`.', 27 | }, 28 | ], 29 | options: [ 30 | 'TODO [0-9]+', 31 | ], 32 | }, 33 | { 34 | code: '// $$FlowFixMeProps I am doing something evil here\nconst text = \'HELLO\';', 35 | errors: [ 36 | { 37 | message: '$FlowFixMe is treated as `any` and must be fixed.', 38 | }, 39 | ], 40 | }, 41 | { 42 | code: '// $FlowFixMeProps I am doing something evil here\nconst text = \'HELLO\';', 43 | errors: [ 44 | { 45 | message: '$FlowFixMe is treated as `any` and must be fixed. Fix it or match `/TODO [0-9]+/u`.', 46 | }, 47 | ], 48 | options: [ 49 | 'TODO [0-9]+', 50 | ], 51 | }, 52 | ], 53 | misconfigured: [ 54 | { 55 | errors: [ 56 | { 57 | data: 7, 58 | instancePath: '/0', 59 | keyword: 'type', 60 | message: 'must be string', 61 | params: { 62 | type: 'string', 63 | }, 64 | parentSchema: { 65 | type: 'string', 66 | }, 67 | schema: 'string', 68 | schemaPath: '#/items/0/type', 69 | }, 70 | ], 71 | options: [7], 72 | }, 73 | ], 74 | valid: [ 75 | { 76 | code: 'const text = \'HELLO\';', 77 | }, 78 | { 79 | code: '// $FlowFixMe TODO 48\nconst text = \'HELLO\';', 80 | options: [ 81 | 'TODO [0-9]+', 82 | ], 83 | }, 84 | ], 85 | }; 86 | -------------------------------------------------------------------------------- /tests/rules/assertions/noInternalFlowType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type X = React$AbstractComponent', 5 | errors: [ 6 | { 7 | message: 'Type identifier \'React$AbstractComponent\' is not allowed. Use \'React.AbstractComponent\' instead.', 8 | }, 9 | ], 10 | }, 11 | { 12 | code: 'type X = React$ChildrenArray', 13 | errors: [ 14 | { 15 | message: 'Type identifier \'React$ChildrenArray\' is not allowed. Use \'React.ChildrenArray\' instead.', 16 | }, 17 | ], 18 | }, 19 | { 20 | code: 'type X = React$ComponentType', 21 | errors: [ 22 | { 23 | message: 'Type identifier \'React$ComponentType\' is not allowed. Use \'React.ComponentType\' instead.', 24 | }, 25 | ], 26 | }, 27 | { 28 | code: 'type X = React$Config', 29 | errors: [{ 30 | message: 'Type identifier \'React$Config\' is not allowed. Use \'React.Config\' instead.', 31 | }], 32 | }, 33 | { 34 | code: 'type X = React$Element', 35 | errors: [{ 36 | message: 'Type identifier \'React$Element\' is not allowed. Use \'React.Element\' instead.', 37 | }], 38 | }, 39 | { 40 | code: 'type X = React$ElementConfig', 41 | errors: [ 42 | { 43 | message: 'Type identifier \'React$ElementConfig\' is not allowed. Use \'React.ElementConfig\' instead.', 44 | }, 45 | ], 46 | }, 47 | { 48 | code: 'type X = React$ElementProps', 49 | errors: [ 50 | { 51 | message: 'Type identifier \'React$ElementProps\' is not allowed. Use \'React.ElementProps\' instead.', 52 | }, 53 | ], 54 | }, 55 | { 56 | code: 'type X = React$ElementRef', 57 | errors: [ 58 | { 59 | message: 'Type identifier \'React$ElementRef\' is not allowed. Use \'React.ElementRef\' instead.', 60 | }, 61 | ], 62 | }, 63 | { 64 | code: 'type X = React$ElementType', 65 | errors: [ 66 | { 67 | message: 'Type identifier \'React$ElementType\' is not allowed. Use \'React.ElementType\' instead.', 68 | }, 69 | ], 70 | }, 71 | { 72 | code: 'type X = React$Key', 73 | errors: [{ 74 | message: 'Type identifier \'React$Key\' is not allowed. Use \'React.Key\' instead.', 75 | }], 76 | }, 77 | { 78 | code: 'type X = React$Node', 79 | errors: [{ 80 | message: 'Type identifier \'React$Node\' is not allowed. Use \'React.Node\' instead.', 81 | }], 82 | }, 83 | { 84 | code: 'type X = React$Ref', 85 | errors: [{ 86 | message: 'Type identifier \'React$Ref\' is not allowed. Use \'React.Ref\' instead.', 87 | }], 88 | }, 89 | { 90 | code: 'type X = React$StatelessFunctionalComponent', 91 | errors: [ 92 | { 93 | message: 'Type identifier \'React$StatelessFunctionalComponent\' is not allowed. Use \'React.StatelessFunctionalComponent\' instead.', 94 | }, 95 | ], 96 | }, 97 | ], 98 | 99 | valid: [ 100 | {code: 'type X = React.AbstractComponent'}, 101 | {code: 'type X = React.ChildrenArray'}, 102 | {code: 'type X = React.ComponentType'}, 103 | {code: 'type X = React.Config'}, 104 | {code: 'type X = React.Element'}, 105 | {code: 'type X = React.ElementConfig'}, 106 | {code: 'type X = React.ElementProps'}, 107 | {code: 'type X = React.ElementRef'}, 108 | {code: 'type X = React.ElementType'}, 109 | {code: 'type X = React.Key'}, 110 | {code: 'type X = React.Node'}, 111 | {code: 'type X = React.Ref'}, 112 | {code: 'type X = React.StatelessFunctionalComponent'}, 113 | 114 | // valid custom type: 115 | {code: 'type X = React$Rocks'}, 116 | ], 117 | }; 118 | -------------------------------------------------------------------------------- /tests/rules/assertions/noMixed.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'function foo(thing): mixed {}', 5 | errors: [{ 6 | message: 'Unexpected use of mixed type', 7 | }], 8 | }, 9 | { 10 | code: 'function foo(thing): Promise {}', 11 | errors: [{ 12 | message: 'Unexpected use of mixed type', 13 | }], 14 | }, 15 | { 16 | code: 'function foo(thing): Promise> {}', 17 | errors: [{ 18 | message: 'Unexpected use of mixed type', 19 | }], 20 | }, 21 | ], 22 | valid: [ 23 | { 24 | code: 'function foo(thing): string {}', 25 | }, 26 | { 27 | code: 'function foo(thing): Promise {}', 28 | }, 29 | { 30 | code: 'function foo(thing): Promise> {}', 31 | }, 32 | { 33 | code: '(foo?: string) => {}', 34 | }, 35 | { 36 | code: '(foo: ?string) => {}', 37 | }, 38 | { 39 | code: '(foo: { a: string }) => {}', 40 | }, 41 | { 42 | code: '(foo: { a: ?string }) => {}', 43 | }, 44 | { 45 | code: '(foo: string[]) => {}', 46 | }, 47 | { 48 | code: 'type Foo = string', 49 | }, 50 | { 51 | code: 'type Foo = { a: string }', 52 | }, 53 | { 54 | code: 'type Foo = { (a: string): string }', 55 | }, 56 | { 57 | code: 'function foo(thing: string) {}', 58 | }, 59 | { 60 | code: 'var foo: string', 61 | }, 62 | { 63 | code: 'class Foo { props: string }', 64 | }, 65 | ], 66 | }; 67 | -------------------------------------------------------------------------------- /tests/rules/assertions/noMutableArray.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type X = Array', 5 | errors: [{message: 'Use "$ReadOnlyArray" instead of "Array"'}], 6 | output: 'type X = $ReadOnlyArray', 7 | }, 8 | { 9 | code: 'type X = string[]', 10 | errors: [{message: 'Use "$ReadOnlyArray" instead of array shorthand notation'}], 11 | output: 'type X = $ReadOnlyArray', 12 | }, 13 | { 14 | code: 'const values: Array> = [];', 15 | errors: [{message: 'Use "$ReadOnlyArray" instead of "Array"'}], 16 | output: 'const values: Array<$ReadOnlyArray> = [];', 17 | }, 18 | { 19 | code: 'let values: Array>;', 20 | errors: [ 21 | {message: 'Use "$ReadOnlyArray" instead of "Array"'}, 22 | {message: 'Use "$ReadOnlyArray" instead of "Array"'}, 23 | ], 24 | output: 'let values: $ReadOnlyArray<$ReadOnlyArray>;', 25 | }, 26 | ], 27 | valid: [ 28 | { 29 | code: 'type X = $ReadOnlyArray', 30 | }, 31 | { 32 | code: 'const values: Array<$ReadOnlyArray> = [];', 33 | }, 34 | { 35 | code: 'const values: $ReadOnlyArray[] = [];', 36 | }, 37 | { 38 | code: 'const values: Array<$ReadOnlyArray> = new Array();', 39 | }, 40 | { 41 | code: 'const values: Array<$ReadOnlyArray> = Array();', 42 | }, 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /tests/rules/assertions/noPrimitiveConstructorTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type x = Number', 5 | errors: [{message: 'Unexpected use of Number constructor type.'}], 6 | }, 7 | { 8 | code: 'type x = String', 9 | errors: [{message: 'Unexpected use of String constructor type.'}], 10 | }, 11 | { 12 | code: 'type x = Boolean', 13 | errors: [{message: 'Unexpected use of Boolean constructor type.'}], 14 | }, 15 | { 16 | code: 'type x = { a: Number }', 17 | errors: [{message: 'Unexpected use of Number constructor type.'}], 18 | }, 19 | { 20 | code: 'type x = { a: String }', 21 | errors: [{message: 'Unexpected use of String constructor type.'}], 22 | }, 23 | { 24 | code: 'type x = { a: Boolean }', 25 | errors: [{message: 'Unexpected use of Boolean constructor type.'}], 26 | }, 27 | { 28 | code: '(x: Number) => {}', 29 | errors: [{message: 'Unexpected use of Number constructor type.'}], 30 | }, 31 | { 32 | code: '(x: String) => {}', 33 | errors: [{message: 'Unexpected use of String constructor type.'}], 34 | }, 35 | { 36 | code: '(x: Boolean) => {}', 37 | errors: [{message: 'Unexpected use of Boolean constructor type.'}], 38 | }, 39 | ], 40 | valid: [ 41 | { 42 | code: 'type x = number', 43 | }, 44 | { 45 | code: 'type x = string', 46 | }, 47 | { 48 | code: 'type x = boolean', 49 | }, 50 | { 51 | code: 'type x = { a: number }', 52 | }, 53 | { 54 | code: 'type x = { a: string }', 55 | }, 56 | { 57 | code: 'type x = { a: boolean }', 58 | }, 59 | { 60 | code: '(x: number) => {}', 61 | }, 62 | { 63 | code: '(x: string) => {}', 64 | }, 65 | { 66 | code: '(x: boolean) => {}', 67 | }, 68 | { 69 | code: 'type x = MyNumber', 70 | }, 71 | { 72 | code: 'type x = MyString', 73 | }, 74 | { 75 | code: 'type x = MyBoolean', 76 | }, 77 | ], 78 | }; 79 | -------------------------------------------------------------------------------- /tests/rules/assertions/noTypesMissingFileAnnotation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'const x: number = 42;', 5 | errors: [{ 6 | message: 'Type annotations require valid Flow declaration.', 7 | }], 8 | }, 9 | { 10 | code: 'type FooType = number;', 11 | errors: [{ 12 | message: 'Type aliases require valid Flow declaration.', 13 | }], 14 | }, 15 | { 16 | code: 'import type A from "a"', 17 | errors: [{ 18 | message: 'Type imports require valid Flow declaration.', 19 | }], 20 | }, 21 | { 22 | code: 'import type {A} from "a"', 23 | errors: [{ 24 | message: 'Type imports require valid Flow declaration.', 25 | }], 26 | }, 27 | { 28 | code: 'import {type A} from "a"', 29 | errors: [{ 30 | message: 'Type imports require valid Flow declaration.', 31 | }], 32 | }, 33 | { 34 | code: 'export type {A} from "a"', 35 | errors: [{ 36 | message: 'Type exports require valid Flow declaration.', 37 | }], 38 | }, 39 | { 40 | code: 'function t(): T{}', 41 | errors: [{ 42 | message: 'Type annotations require valid Flow declaration.', 43 | }], 44 | }, 45 | { 46 | code: 'const x: number = 42;', 47 | errors: [{ 48 | message: 'Type annotations require valid Flow declaration.', 49 | }], 50 | settings: { 51 | flowtype: { 52 | onlyFilesWithFlowAnnotation: true, 53 | }, 54 | }, 55 | }, 56 | ], 57 | valid: [ 58 | { 59 | code: '// @flow\nconst x: number = 42;', 60 | }, 61 | { 62 | code: '/* @flow weak */\ntype FooType = number;', 63 | }, 64 | { 65 | code: '/* @noflow */\ntype FooType = number;', 66 | }, 67 | { 68 | code: '/* @noflow */\nimport type A from "a"', 69 | }, 70 | { 71 | code: '/* @noflow */\nimport {type A} from "a"', 72 | }, 73 | { 74 | code: '/* @noflow */\nexport type {A} from "a"', 75 | }, 76 | { 77 | code: '// an unrelated comment\n// @flow\nexport type {A} from "a"', 78 | }, 79 | ], 80 | }; 81 | -------------------------------------------------------------------------------- /tests/rules/assertions/noUnusedExpressions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'foo + 1', 5 | errors: [{ 6 | message: 'Expected an assignment or function call and instead saw an expression.', 7 | }], 8 | }, 9 | { 10 | code: 'x?.y', 11 | errors: [{ 12 | message: 'Expected an assignment or function call and instead saw an expression.', 13 | }], 14 | }, 15 | ], 16 | valid: [ 17 | { 18 | code: '(foo: number)', 19 | }, 20 | { 21 | code: 'x?.y()', 22 | }, 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /tests/rules/assertions/quotes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type T = \'hi\'', 5 | errors: [ 6 | { 7 | message: 'String literals must use double quote.', 8 | }, 9 | ], 10 | output: 'type T = "hi"', 11 | }, 12 | { 13 | code: 'type T = { test: \'hello\' | \'test\' }', 14 | errors: [ 15 | { 16 | message: 'String literals must use double quote.', 17 | }, 18 | { 19 | message: 'String literals must use double quote.', 20 | }, 21 | ], 22 | options: ['double'], 23 | output: 'type T = { test: "hello" | "test" }', 24 | }, 25 | { 26 | code: 'type T = { test: "hello" | \'test\', t: \'hello\' }', 27 | errors: [ 28 | { 29 | message: 'String literals must use double quote.', 30 | }, 31 | { 32 | message: 'String literals must use double quote.', 33 | }, 34 | ], 35 | options: ['double'], 36 | output: 'type T = { test: "hello" | "test", t: "hello" }', 37 | }, 38 | { 39 | code: 'type T = "hi"', 40 | errors: [ 41 | { 42 | message: 'String literals must use single quote.', 43 | }, 44 | ], 45 | options: ['single'], 46 | output: 'type T = \'hi\'', 47 | }, 48 | { 49 | code: 'type T = { test: "hello" | "test" }', 50 | errors: [ 51 | { 52 | message: 'String literals must use single quote.', 53 | }, 54 | { 55 | message: 'String literals must use single quote.', 56 | }, 57 | ], 58 | options: ['single'], 59 | output: 'type T = { test: \'hello\' | \'test\' }', 60 | }, 61 | { 62 | code: 'type T = { test: "hello" | \'test\', t: \'hello\' }', 63 | errors: [ 64 | { 65 | message: 'String literals must use single quote.', 66 | }, 67 | ], 68 | options: ['single'], 69 | output: 'type T = { test: \'hello\' | \'test\', t: \'hello\' }', 70 | }, 71 | ], 72 | misconfigured: [ 73 | { 74 | errors: [ 75 | { 76 | data: 'temporarily', 77 | instancePath: '/0', 78 | keyword: 'enum', 79 | message: 'must be equal to one of the allowed values', 80 | params: { 81 | allowedValues: [ 82 | 'double', 83 | 'single', 84 | ], 85 | }, 86 | parentSchema: { 87 | enum: [ 88 | 'double', 89 | 'single', 90 | ], 91 | type: 'string', 92 | }, 93 | schema: [ 94 | 'double', 95 | 'single', 96 | ], 97 | schemaPath: '#/items/0/enum', 98 | }, 99 | ], 100 | options: ['temporarily'], 101 | }, 102 | ], 103 | valid: [ 104 | { 105 | code: 'type T = "hi"', 106 | options: ['double'], 107 | }, 108 | { 109 | code: 'type T = { test: "hello" | "test" }', 110 | options: ['double'], 111 | }, 112 | { 113 | code: 'type T = { test: "hello" | "test", t: "hello" }', 114 | options: ['double'], 115 | }, 116 | { 117 | code: 'type FooType = \'hi\'', 118 | options: ['single'], 119 | }, 120 | { 121 | code: 'type T = { test: \'hello\' | \'test\' }', 122 | options: ['single'], 123 | }, 124 | { 125 | code: 'type T = { test: \'hello\' | \'test\', t: \'hello\' }', 126 | options: ['single'], 127 | }, 128 | ], 129 | }; 130 | -------------------------------------------------------------------------------- /tests/rules/assertions/requireCompoundTypeAlias.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'const foo: string | null = null;', 5 | errors: [{message: 'All union types must be declared with named type alias.'}], 6 | options: [ 7 | 'always', 8 | { 9 | allowNull: false, 10 | }, 11 | ], 12 | }, 13 | { 14 | code: 'function foo(bar: "A" | "B") {}', 15 | errors: [{message: 'All union types must be declared with named type alias.'}], 16 | }, 17 | { 18 | code: 'const foo: "A" | "B" = "A";', 19 | errors: [{message: 'All union types must be declared with named type alias.'}], 20 | }, 21 | { 22 | code: 'type Foo = { bar: "A" | "B" };', 23 | errors: [{message: 'All union types must be declared with named type alias.'}], 24 | }, 25 | { 26 | code: 'function foo(bar: { n: number } | { s: string }) {}', 27 | errors: [{message: 'All union types must be declared with named type alias.'}], 28 | }, 29 | { 30 | code: 'function foo(bar: { n: number } & { s: string }) {}', 31 | errors: [{message: 'All intersection types must be declared with named type alias.'}], 32 | }, 33 | { 34 | code: 'const foo: { n: number } & { s: string } = { n: 0, s: "" };', 35 | errors: [{message: 'All intersection types must be declared with named type alias.'}], 36 | }, 37 | { 38 | code: 'type Foo = { bar: { n: number } & { s: string } };', 39 | errors: [{message: 'All intersection types must be declared with named type alias.'}], 40 | }, 41 | { 42 | code: 'function foo(bar: { n: number } & { s: string }) {}', 43 | errors: [{message: 'All intersection types must be declared with named type alias.'}], 44 | }, 45 | ], 46 | misconfigured: [ 47 | { 48 | errors: [ 49 | { 50 | data: 'sometimes', 51 | instancePath: '/0', 52 | keyword: 'enum', 53 | message: 'must be equal to one of the allowed values', 54 | params: { 55 | allowedValues: [ 56 | 'always', 57 | 'never', 58 | ], 59 | }, 60 | parentSchema: { 61 | enum: [ 62 | 'always', 63 | 'never', 64 | ], 65 | type: 'string', 66 | }, 67 | schema: [ 68 | 'always', 69 | 'never', 70 | ], 71 | schemaPath: '#/items/0/enum', 72 | }, 73 | ], 74 | options: ['sometimes'], 75 | }, 76 | ], 77 | valid: [ 78 | { 79 | code: 'const foo: string | null = null;', 80 | }, 81 | { 82 | code: 'const foo: string | null = null;', 83 | options: [ 84 | 'always', 85 | { 86 | allowNull: true, 87 | }, 88 | ], 89 | }, 90 | { 91 | code: 'type Foo = "A" | "B";', 92 | }, 93 | { 94 | code: 'type Bar = "A" | "B"; function foo(bar: Bar) {}', 95 | }, 96 | { 97 | code: 'type Foo = { disjoint: "A", n: number } | { disjoint: "B", s: string };', 98 | }, 99 | { 100 | code: 'type Foo = { n: number } & { s: string };', 101 | }, 102 | { 103 | code: 'type Bar = { n: number } & { s: string }; function foo(bar: Bar) {}', 104 | }, 105 | { 106 | code: 'function foo(bar: "A" | "B") {}', 107 | options: ['never'], 108 | }, 109 | { 110 | code: 'function foo(bar: { n: number } & { s: string }) {}', 111 | options: ['never'], 112 | }, 113 | ], 114 | }; 115 | -------------------------------------------------------------------------------- /tests/rules/assertions/requireIndexerName.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type foo = { [string]: number };', 5 | errors: [ 6 | {message: 'All indexers must be declared with key name.'}, 7 | ], 8 | output: 'type foo = { [key: string]: number };', 9 | }, 10 | ], 11 | valid: [ 12 | { 13 | code: 'type foo = { [key: string]: number };', 14 | errors: [], 15 | }, 16 | { 17 | code: 'type foo = { [key: string]: number };', 18 | errors: [], 19 | options: ['never'], 20 | }, 21 | { 22 | code: 'type foo = { [string]: number };', 23 | errors: [], 24 | options: ['never'], 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /tests/rules/assertions/requireInexactType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | // Always 4 | 5 | { 6 | code: 'type foo = {};', 7 | errors: [ 8 | { 9 | message: 'Type must be explicit inexact.', 10 | }, 11 | ], 12 | }, 13 | { 14 | code: 'type foo = { bar: string };', 15 | errors: [ 16 | { 17 | message: 'Type must be explicit inexact.', 18 | }, 19 | ], 20 | }, 21 | { 22 | code: 'type foo = {};', 23 | errors: [ 24 | { 25 | message: 'Type must be explicit inexact.', 26 | }, 27 | ], 28 | options: ['always'], 29 | }, 30 | { 31 | code: 'type foo = { bar: string };', 32 | errors: [ 33 | { 34 | message: 'Type must be explicit inexact.', 35 | }, 36 | ], 37 | options: ['always'], 38 | }, 39 | 40 | // Never 41 | 42 | { 43 | code: 'type foo = {...};', 44 | errors: [ 45 | { 46 | message: 'Type must not be explicit inexact.', 47 | }, 48 | ], 49 | options: ['never'], 50 | }, 51 | { 52 | code: 'type foo = { bar: string, ... };', 53 | errors: [ 54 | { 55 | message: 'Type must not be explicit inexact.', 56 | }, 57 | ], 58 | options: ['never'], 59 | }, 60 | ], 61 | valid: [ 62 | 63 | // Always 64 | 65 | { 66 | code: 'type foo = { foo: string, ... };', 67 | }, 68 | { 69 | code: 'interface Foo { foo: string }', 70 | }, 71 | { 72 | code: 'declare class Foo { foo: string }', 73 | }, 74 | { 75 | code: 'type foo = {| |};', 76 | }, 77 | { 78 | code: 'type foo = {| bar: string |};', 79 | }, 80 | { 81 | code: 'type foo = { [key: string]: string, ... };', 82 | }, 83 | { 84 | code: 'type foo = number;', 85 | }, 86 | { 87 | code: 'type foo = {| |};', 88 | options: ['always'], 89 | }, 90 | { 91 | code: 'type foo = {...};', 92 | options: ['always'], 93 | }, 94 | { 95 | code: 'type foo = { bar: string, ... };', 96 | options: ['always'], 97 | }, 98 | { 99 | code: 'type foo = {| bar: string |};', 100 | options: ['always'], 101 | }, 102 | { 103 | code: 'type foo = number;', 104 | options: ['always'], 105 | }, 106 | 107 | // Never 108 | 109 | { 110 | code: 'type foo = { };', 111 | options: ['never'], 112 | }, 113 | { 114 | code: 'type foo = {| |};', 115 | options: ['never'], 116 | }, 117 | { 118 | code: 'type foo = { bar: string };', 119 | options: ['never'], 120 | }, 121 | { 122 | code: 'type foo = {| bar: string |};', 123 | options: ['never'], 124 | }, 125 | { 126 | code: 'type foo = number;', 127 | options: ['never'], 128 | }, 129 | ], 130 | }; 131 | -------------------------------------------------------------------------------- /tests/rules/assertions/requireTypesAtTop.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'const foo = 3;\ntype Foo = number;', 5 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 6 | }, 7 | { 8 | code: 'const foo = 3;\nopaque type Foo = number;', 9 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 10 | }, 11 | { 12 | code: 'const foo = 3;\nexport type Foo = number;', 13 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 14 | }, 15 | { 16 | code: 'const foo = 3;\nexport opaque type Foo = number;', 17 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 18 | }, 19 | { 20 | code: 'const foo = 3;\ntype Foo = number | string;', 21 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 22 | }, 23 | { 24 | code: 'import bar from "./bar";\nconst foo = 3;\ntype Foo = number;', 25 | errors: [{message: 'All type declaration must be at the top of the file, after any import declarations.'}], 26 | }, 27 | ], 28 | misconfigured: [ 29 | { 30 | errors: [ 31 | { 32 | data: 'sometimes', 33 | instancePath: '/0', 34 | keyword: 'enum', 35 | message: 'must be equal to one of the allowed values', 36 | params: { 37 | allowedValues: [ 38 | 'always', 39 | 'never', 40 | ], 41 | }, 42 | parentSchema: { 43 | enum: [ 44 | 'always', 45 | 'never', 46 | ], 47 | type: 'string', 48 | }, 49 | schema: [ 50 | 'always', 51 | 'never', 52 | ], 53 | schemaPath: '#/items/0/enum', 54 | }, 55 | ], 56 | options: ['sometimes'], 57 | }, 58 | ], 59 | valid: [ 60 | { 61 | code: 'type Foo = number;\nconst foo = 3;', 62 | }, 63 | { 64 | code: 'opaque type Foo = number;\nconst foo = 3;', 65 | }, 66 | { 67 | code: 'export type Foo = number;\nconst foo = 3;', 68 | }, 69 | { 70 | code: 'export opaque type Foo = number;\nconst foo = 3;', 71 | }, 72 | { 73 | code: 'type Foo = number;\nconst foo = 3;', 74 | }, 75 | { 76 | code: 'import bar from "./bar";\ntype Foo = number;', 77 | }, 78 | { 79 | code: 'type Foo = number;\nimport bar from "./bar";', 80 | }, 81 | { 82 | code: 'const foo = 3;\ntype Foo = number;', 83 | options: ['never'], 84 | }, 85 | ], 86 | }; 87 | -------------------------------------------------------------------------------- /tests/rules/assertions/semi.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'class Foo { foo: string }', 5 | errors: [ 6 | { 7 | message: 'Missing semicolon.', 8 | }, 9 | ], 10 | options: ['always'], 11 | output: 'class Foo { foo: string; }', 12 | }, 13 | { 14 | code: 'class Foo { foo: string; }', 15 | errors: [ 16 | { 17 | message: 'Extra semicolon.', 18 | }, 19 | ], 20 | options: ['never'], 21 | output: 'class Foo { foo: string }', 22 | }, 23 | { 24 | code: 'type FooType = {}', 25 | errors: [ 26 | { 27 | message: 'Missing semicolon.', 28 | }, 29 | ], 30 | options: [], 31 | output: 'type FooType = {};', 32 | }, 33 | { 34 | code: 'type FooType = {}', 35 | errors: [ 36 | { 37 | message: 'Missing semicolon.', 38 | }, 39 | ], 40 | options: ['always'], 41 | output: 'type FooType = {};', 42 | }, 43 | { 44 | code: 'type FooType = {};', 45 | errors: [ 46 | { 47 | message: 'Extra semicolon.', 48 | }, 49 | ], 50 | options: ['never'], 51 | output: 'type FooType = {}', 52 | }, 53 | { 54 | code: 'opaque type FooType = {}', 55 | errors: [ 56 | { 57 | message: 'Missing semicolon.', 58 | }, 59 | ], 60 | options: [], 61 | output: 'opaque type FooType = {};', 62 | }, 63 | ], 64 | misconfigured: [ 65 | { 66 | errors: [ 67 | { 68 | data: 'temporarily', 69 | instancePath: '/0', 70 | keyword: 'enum', 71 | message: 'must be equal to one of the allowed values', 72 | params: { 73 | allowedValues: [ 74 | 'always', 75 | 'never', 76 | ], 77 | }, 78 | parentSchema: { 79 | enum: [ 80 | 'always', 81 | 'never', 82 | ], 83 | type: 'string', 84 | }, 85 | schema: [ 86 | 'always', 87 | 'never', 88 | ], 89 | schemaPath: '#/items/0/enum', 90 | }, 91 | ], 92 | options: ['temporarily'], 93 | }, 94 | ], 95 | valid: [ 96 | { 97 | code: 'type FooType = {};', 98 | }, 99 | { 100 | code: 'type FooType = {};', 101 | options: ['always'], 102 | }, 103 | { 104 | code: '(foo: string) => {}', 105 | options: ['always'], 106 | }, 107 | { 108 | code: 'class Foo { foo: string; }', 109 | options: ['always'], 110 | }, 111 | { 112 | code: 'class Foo { foo: string }', 113 | options: ['never'], 114 | }, 115 | { 116 | code: 'type FooType = { a: number;\n b: string;\n };', 117 | options: ['always'], 118 | }, 119 | { 120 | code: 'type FooType = { a: number;\n b: string;\n }', 121 | options: ['never'], 122 | }, 123 | { 124 | code: 'type FooType = {}', 125 | options: ['never'], 126 | }, 127 | { 128 | code: 'type FooType = {}', 129 | settings: { 130 | flowtype: { 131 | onlyFilesWithFlowAnnotation: true, 132 | }, 133 | }, 134 | }, 135 | { 136 | code: 'opaque type FooType = {};', 137 | }, 138 | ], 139 | }; 140 | -------------------------------------------------------------------------------- /tests/rules/assertions/sortTypeUnionIntersectionMembers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type T1 = B | A;', 5 | errors: [{message: 'Expected union members to be in ascending order. "A" should be before "B".'}], 6 | output: 'type T1 = A | B;', 7 | }, 8 | { 9 | code: 'type T2 = { b: string } & { a: string };', 10 | errors: [{message: 'Expected intersection members to be in ascending order. "{ a: string }" should be before "{ b: string }".'}], 11 | output: 'type T2 = { a: string } & { b: string };', 12 | }, 13 | { 14 | code: 'type T3 = [1, 2, 4] & [1, 2, 3];', 15 | errors: [{message: 'Expected intersection members to be in ascending order. "[1, 2, 3]" should be before "[1, 2, 4]".'}], 16 | output: 'type T3 = [1, 2, 3] & [1, 2, 4];', 17 | }, 18 | { 19 | code: ` 20 | type T4 = 21 | | [1, 2, 4] 22 | | [1, 2, 3] 23 | | { b: string } 24 | | { a: string } 25 | | (() => void) 26 | | (() => string) 27 | | 'b' 28 | | 'a' 29 | | 'b' 30 | | 'a' 31 | | string[] 32 | | number[] 33 | | B 34 | | A 35 | | string 36 | | any; 37 | `, 38 | errors: [ 39 | {message: 'Expected union members to be in ascending order. "[1, 2, 3]" should be before "[1, 2, 4]".'}, 40 | {message: 'Expected union members to be in ascending order. "{ b: string }" should be before "[1, 2, 3]".'}, 41 | {message: 'Expected union members to be in ascending order. "{ a: string }" should be before "{ b: string }".'}, 42 | {message: 'Expected union members to be in ascending order. "() => void" should be before "{ a: string }".'}, 43 | {message: 'Expected union members to be in ascending order. "() => string" should be before "() => void".'}, 44 | {message: 'Expected union members to be in ascending order. "\'b\'" should be before "() => string".'}, 45 | {message: 'Expected union members to be in ascending order. "\'a\'" should be before "\'b\'".'}, 46 | {message: 'Expected union members to be in ascending order. "\'b\'" should be before "\'a\'".'}, 47 | {message: 'Expected union members to be in ascending order. "\'a\'" should be before "\'b\'".'}, 48 | {message: 'Expected union members to be in ascending order. "string[]" should be before "\'a\'".'}, 49 | {message: 'Expected union members to be in ascending order. "number[]" should be before "string[]".'}, 50 | {message: 'Expected union members to be in ascending order. "B" should be before "number[]".'}, 51 | {message: 'Expected union members to be in ascending order. "A" should be before "B".'}, 52 | {message: 'Expected union members to be in ascending order. "string" should be before "A".'}, 53 | {message: 'Expected union members to be in ascending order. "any" should be before "string".'}, 54 | ], 55 | output: ` 56 | type T4 = 57 | any | string | A | B | number[] | string[] | 'a' | 'a' | 'b' | 'b' | () => string | () => void | { a: string } | { b: string } | [1, 2, 3] | [1, 2, 4]; 58 | `, 59 | }, 60 | ], 61 | valid: [ 62 | { 63 | code: 'type T1 = A | B;', 64 | }, 65 | { 66 | code: 'type T2 = { a: string } & { b: string };', 67 | }, 68 | { 69 | code: 'type T3 = [1, 2, 3] & [1, 2, 4];', 70 | }, 71 | { 72 | code: ` 73 | type T4 = 74 | | any 75 | | string 76 | | A 77 | | B 78 | | number[] 79 | | string[] 80 | | 'a' 81 | | 'a' 82 | | 'b' 83 | | 'b' 84 | | (() => string) 85 | | (() => void) 86 | | { a: string } 87 | | { b: string } 88 | | [1, 2, 3] 89 | | [1, 2, 4]; 90 | `, 91 | }, 92 | ], 93 | }; 94 | -------------------------------------------------------------------------------- /tests/rules/assertions/spaceBeforeGenericBracket.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type X = Promise ', 5 | errors: [{message: 'There must be no space before "Promise" generic type annotation bracket'}], 6 | output: 'type X = Promise', 7 | }, 8 | { 9 | code: 'type X = Promise ', 10 | errors: [{message: 'There must be no space before "Promise" generic type annotation bracket'}], 11 | options: ['never'], 12 | output: 'type X = Promise', 13 | }, 14 | { 15 | code: 'type X = Promise ', 16 | errors: [{message: 'There must be no space before "Promise" generic type annotation bracket'}], 17 | output: 'type X = Promise', 18 | }, 19 | { 20 | code: 'type X = Promise', 21 | errors: [{message: 'There must be a space before "Promise" generic type annotation bracket'}], 22 | options: ['always'], 23 | output: 'type X = Promise ', 24 | }, 25 | { 26 | code: 'type X = Promise ', 27 | errors: [{message: 'There must be one space before "Promise" generic type annotation bracket'}], 28 | options: ['always'], 29 | output: 'type X = Promise ', 30 | }, 31 | ], 32 | misconfigured: [ 33 | { 34 | errors: [ 35 | { 36 | data: 'whenever', 37 | instancePath: '/0', 38 | keyword: 'enum', 39 | message: 'must be equal to one of the allowed values', 40 | params: { 41 | allowedValues: [ 42 | 'always', 43 | 'never', 44 | ], 45 | }, 46 | parentSchema: { 47 | enum: [ 48 | 'always', 49 | 'never', 50 | ], 51 | type: 'string', 52 | }, 53 | schema: [ 54 | 'always', 55 | 'never', 56 | ], 57 | schemaPath: '#/items/0/enum', 58 | }, 59 | ], 60 | options: ['whenever'], 61 | }, 62 | ], 63 | valid: [ 64 | { 65 | code: 'type X = Promise', 66 | }, 67 | { 68 | code: 'type X = Promise ', 69 | options: ['always'], 70 | }, 71 | ], 72 | }; 73 | -------------------------------------------------------------------------------- /tests/rules/assertions/spreadExactType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'type bar = {...{test: string}}', 5 | errors: [{message: 'Use $Exact to make type spreading safe.'}], 6 | }, 7 | { 8 | code: 'type foo = {test: number}; type bar = {...foo}', 9 | errors: [{message: 'Use $Exact to make type spreading safe.'}], 10 | }, 11 | ], 12 | valid: [ 13 | { 14 | code: 'type bar = {...$Exact<{test: string}>}', 15 | }, 16 | { 17 | code: 'type foo = {test: number}; type bar = {...$Exact}', 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /tests/rules/assertions/typeIdMatch.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'opaque type foo = {};', 5 | errors: [ 6 | { 7 | message: 'Type identifier \'foo\' does not match pattern \'/^([A-Z][a-z0-9]*)+Type$/u\'.', 8 | }, 9 | ], 10 | }, 11 | { 12 | code: 'type foo = {};', 13 | errors: [ 14 | { 15 | message: 'Type identifier \'foo\' does not match pattern \'/^([A-Z][a-z0-9]*)+Type$/u\'.', 16 | }, 17 | ], 18 | }, 19 | { 20 | code: 'type FooType = {};', 21 | errors: [ 22 | { 23 | message: 'Type identifier \'FooType\' does not match pattern \'/^foo$/u\'.', 24 | }, 25 | ], 26 | options: [ 27 | '^foo$', 28 | ], 29 | }, 30 | ], 31 | misconfigured: [ 32 | { 33 | errors: [ 34 | { 35 | data: 7, 36 | instancePath: '/0', 37 | keyword: 'type', 38 | message: 'must be string', 39 | params: { 40 | type: 'string', 41 | }, 42 | parentSchema: { 43 | type: 'string', 44 | }, 45 | schema: 'string', 46 | schemaPath: '#/items/0/type', 47 | }, 48 | ], 49 | options: [7], 50 | }, 51 | ], 52 | valid: [ 53 | { 54 | code: 'type FooType = {};', 55 | }, 56 | { 57 | code: 'type foo = {};', 58 | options: [ 59 | '^foo$', 60 | ], 61 | }, 62 | { 63 | code: 'type foo = {};', 64 | settings: { 65 | flowtype: { 66 | onlyFilesWithFlowAnnotation: true, 67 | }, 68 | }, 69 | }, 70 | ], 71 | }; 72 | -------------------------------------------------------------------------------- /tests/rules/assertions/typeImportStyle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: 'import type {A, B} from \'a\';', 5 | errors: [{message: 'Unexpected "import type"'}], 6 | output: 'import {type A, type B} from \'a\';', 7 | }, 8 | { 9 | code: 'import type {A, B} from \'a\';', 10 | errors: [{message: 'Unexpected "import type"'}], 11 | options: ['identifier'], 12 | output: 'import {type A, type B} from \'a\';', 13 | }, 14 | { 15 | code: 'import type {A, B as C} from \'a\';', 16 | errors: [{message: 'Unexpected "import type"'}], 17 | options: ['identifier'], 18 | output: 'import {type A, type B as C} from \'a\';', 19 | }, 20 | { 21 | code: 'import type A from \'a\';', 22 | errors: [{message: 'Unexpected "import type"'}], 23 | options: ['identifier'], 24 | output: 'import {type default as A} from \'a\';', 25 | }, 26 | { 27 | code: 'import {type A, type B} from \'a\';', 28 | errors: [ 29 | {message: 'Unexpected type import'}, 30 | {message: 'Unexpected type import'}, 31 | ], 32 | options: ['declaration'], 33 | }, 34 | ], 35 | valid: [ 36 | { 37 | code: 'import {type A, type B} from \'a\';', 38 | }, 39 | { 40 | code: 'import {type A, type B} from \'a\';', 41 | options: ['identifier'], 42 | }, 43 | { 44 | code: 'import type {A, B} from \'a\';', 45 | options: ['declaration'], 46 | }, 47 | { 48 | code: 'import typeof * as A from \'a\';', 49 | options: ['identifier'], 50 | }, 51 | { 52 | code: 'import type A from \'a\';', 53 | options: ['identifier', {ignoreTypeDefault: true}], 54 | }, 55 | { 56 | code: 'declare module "m" { import type A from \'a\'; }', 57 | options: ['identifier'], 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /tests/rules/assertions/useReadOnlySpread.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | { 4 | code: `type INode = {||}; 5 | type Identifier = {| 6 | ...INode, 7 | +aaa: string, 8 | |};`, 9 | errors: [{ 10 | message: 'Flow type with spread property and all readonly properties must be wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.', 11 | }], 12 | }, 13 | { 14 | code: `type INode = {||}; 15 | type Identifier = {| 16 | ...INode, 17 | +aaa: string, 18 | +bbb: string, 19 | |};`, 20 | errors: [{ 21 | message: 'Flow type with spread property and all readonly properties must be wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.', 22 | }], 23 | }, 24 | ], 25 | 26 | valid: [ 27 | // Object with spread operator: 28 | { 29 | code: `type INode = {||}; 30 | type Identifier = {| 31 | ...INode, 32 | name: string, 33 | |};`, 34 | }, 35 | { 36 | code: `type INode = {||}; 37 | type Identifier = {| 38 | ...INode, 39 | name: string, // writable on purpose 40 | +surname: string, 41 | |};`, 42 | }, 43 | 44 | // Object without spread operator: 45 | { 46 | code: `type Identifier = {| 47 | +name: string, 48 | |};`, 49 | }, 50 | 51 | // Read-only object with spread: 52 | { 53 | code: `type INode = {||}; 54 | type Identifier = $ReadOnly<{| 55 | ...INode, 56 | +name: string, 57 | |}>;`, 58 | }, 59 | { 60 | code: `type INode = {||}; 61 | type Identifier = $ReadOnly<{| 62 | ...INode, 63 | name: string, // writable on purpose 64 | |}>;`, 65 | }, 66 | 67 | // Write-only object with spread: 68 | { 69 | code: `type INode = {||}; 70 | type Identifier = $ReadOnly<{| 71 | ...INode, 72 | -name: string, 73 | |}>;`, 74 | }, 75 | ], 76 | }; 77 | -------------------------------------------------------------------------------- /tests/rules/assertions/validSyntax.js: -------------------------------------------------------------------------------- 1 | export default { 2 | invalid: [ 3 | 4 | // removed, as Babylon now prevents the invalid syntax 5 | ], 6 | valid: [ 7 | { 8 | code: 'function x(foo: string = "1") {}', 9 | }, 10 | { 11 | code: 'function x(foo: Type = bar()) {}', 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /tests/rules/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Ajv from 'ajv'; 3 | import { 4 | RuleTester, 5 | } from 'eslint'; 6 | import { 7 | camelCase, 8 | } from 'lodash'; 9 | import plugin from '../../src'; 10 | 11 | const ruleTester = new RuleTester({ 12 | parserOptions: { 13 | babelOptions: { 14 | plugins: [ 15 | '@babel/plugin-transform-react-jsx', 16 | '@babel/plugin-syntax-flow', 17 | ], 18 | }, 19 | requireConfigFile: false, 20 | }, 21 | }); 22 | 23 | const reportingRules = [ 24 | 'array-style-complex-type', 25 | 'array-style-simple-type', 26 | 'arrow-parens', 27 | 'boolean-style', 28 | 'define-flow-type', 29 | 'delimiter-dangle', 30 | 'enforce-line-break', 31 | 'generic-spacing', 32 | 'interface-id-match', 33 | 'newline-after-flow-annotation', 34 | 'no-dupe-keys', 35 | 'no-duplicate-type-union-intersection-members', 36 | 'no-existential-type', 37 | 'no-flow-fix-me-comments', 38 | 'no-mutable-array', 39 | 'no-primitive-constructor-types', 40 | 'no-types-missing-file-annotation', 41 | 'no-unused-expressions', 42 | 'no-weak-types', 43 | 'no-internal-flow-type', 44 | 'no-mixed', 45 | 'object-type-curly-spacing', 46 | 'object-type-delimiter', 47 | 'quotes', 48 | 'require-compound-type-alias', 49 | 'require-inexact-type', 50 | 'require-indexer-name', 51 | 'require-exact-type', 52 | 'require-parameter-type', 53 | 'require-readonly-react-props', 54 | 'require-return-type', 55 | 'require-types-at-top', 56 | 'require-valid-file-annotation', 57 | 'require-variable-type', 58 | 'semi', 59 | 'sort-keys', 60 | 'sort-type-union-intersection-members', 61 | 'space-after-type-colon', 62 | 'space-before-generic-bracket', 63 | 'space-before-type-colon', 64 | 'spread-exact-type', 65 | 'type-id-match', 66 | 'type-import-style', 67 | 'union-intersection-spacing', 68 | 'use-flow-type', 69 | 'use-read-only-spread', 70 | 'valid-syntax', 71 | ]; 72 | 73 | const parser = require.resolve('@babel/eslint-parser'); 74 | const ajv = new Ajv({ 75 | verbose: true, 76 | }); 77 | 78 | for (const ruleName of reportingRules) { 79 | // eslint-disable-next-line import/no-dynamic-require 80 | const assertions = require('./assertions/' + camelCase(ruleName)); 81 | 82 | if (assertions.misconfigured) { 83 | for (const misconfiguration of assertions.misconfigured) { 84 | RuleTester.describe(ruleName, () => { 85 | RuleTester.describe('misconfigured', () => { 86 | RuleTester.it(JSON.stringify(misconfiguration.options), () => { 87 | const schema = plugin.rules[ruleName].schema && plugin.rules[ruleName].schema; 88 | 89 | if (!schema) { 90 | throw new Error('No schema.'); 91 | } 92 | 93 | const validateSchema = ajv.compile({ 94 | items: schema, 95 | type: 'array', 96 | }); 97 | 98 | validateSchema(misconfiguration.options); 99 | if (!validateSchema.errors) { 100 | throw new Error('Schema was valid.'); 101 | } 102 | 103 | assert.deepStrictEqual(validateSchema.errors, misconfiguration.errors); 104 | }); 105 | }); 106 | }); 107 | } 108 | } 109 | 110 | assertions.invalid = assertions.invalid.map((assertion) => { 111 | assertion.parser = parser; 112 | 113 | return assertion; 114 | }); 115 | 116 | assertions.valid = assertions.valid.map((assertion) => { 117 | assertion.parser = parser; 118 | 119 | return assertion; 120 | }); 121 | 122 | ruleTester.run(ruleName, plugin.rules[ruleName], assertions); 123 | } 124 | --------------------------------------------------------------------------------