diff --git a/.README/README.md b/.README/README.md index 66cbb294..c6417646 100644 --- a/.README/README.md +++ b/.README/README.md @@ -154,6 +154,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d {"gitdown": "include", "file": "./rules/no-unused-expressions.md"} {"gitdown": "include", "file": "./rules/no-weak-types.md"} {"gitdown": "include", "file": "./rules/object-type-delimiter.md"} +{"gitdown": "include", "file": "./rules/require-exact-type.md"} {"gitdown": "include", "file": "./rules/require-parameter-type.md"} {"gitdown": "include", "file": "./rules/require-return-type.md"} {"gitdown": "include", "file": "./rules/require-valid-file-annotation.md"} diff --git a/.README/rules/require-exact-type.md b/.README/rules/require-exact-type.md new file mode 100644 index 00000000..f5ce313f --- /dev/null +++ b/.README/rules/require-exact-type.md @@ -0,0 +1,32 @@ +### `require-exact-type` + +This rule enforces [exact object types](https://flow.org/en/docs/types/objects/#toc-exact-object-types). + +#### Options + +The rule has one string option: + +* `"always"` (default): Report all object type definitions that aren't exact. +* `"never"`: Report all object type definitions that are exact. + +```js +{ + "rules": { + "flowtype/require-exact-type": [ + 2, + "always" + ] + } +} + +{ + "rules": { + "flowtype/require-exact-type": [ + 2, + "never" + ] + } +} +``` + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46bc7151..9c72ff4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ Run with `npm run lint`. 1. Create a file in `tests/rules/assertions` named the `camelCase` version of your rule name with the following template: * `export default { invalid: [], valid: [] }` -2. Add your test file to `tests/index.js` +2. Add your test file to `tests/rules/index.js` 3. Create a file in `src/rules` named the `camelCase` version of your rule name 4. Add your rule file to `src/index.js` diff --git a/README.md b/README.md index d07732bb..d194ff1d 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ * [`delimiter-dangle`](#eslint-plugin-flowtype-rules-delimiter-dangle) * [`generic-spacing`](#eslint-plugin-flowtype-rules-generic-spacing) * [`no-dupe-keys`](#eslint-plugin-flowtype-rules-no-dupe-keys) + * [`no-flow-fix-me-comments`](#eslint-plugin-flowtype-rules-no-flow-fix-me-comments) * [`no-mutable-array`](#eslint-plugin-flowtype-rules-no-mutable-array) * [`no-primitive-constructor-types`](#eslint-plugin-flowtype-rules-no-primitive-constructor-types) * [`no-types-missing-file-annotation`](#eslint-plugin-flowtype-rules-no-types-missing-file-annotation) * [`no-unused-expressions`](#eslint-plugin-flowtype-rules-no-unused-expressions) * [`no-weak-types`](#eslint-plugin-flowtype-rules-no-weak-types) * [`object-type-delimiter`](#eslint-plugin-flowtype-rules-object-type-delimiter) + * [`require-exact-type`](#eslint-plugin-flowtype-rules-require-exact-type) * [`require-parameter-type`](#eslint-plugin-flowtype-rules-require-parameter-type) * [`require-return-type`](#eslint-plugin-flowtype-rules-require-return-type) * [`require-valid-file-annotation`](#eslint-plugin-flowtype-rules-require-valid-file-annotation) @@ -252,6 +254,9 @@ var a: AType type A = AType // Additional rules: {"no-undef":2} +declare type A = number +// Additional rules: {"no-undef":2} + opaque type A = AType // Additional rules: {"no-undef":2} @@ -285,6 +290,9 @@ class C implements AType {} interface AType {} // Additional rules: {"no-undef":2} +declare interface A {} +// Additional rules: {"no-undef":2} + ({ a: ({b() {}}: AType) }) // Additional rules: {"no-undef":2} @@ -309,6 +317,9 @@ var a: AType type A = AType // Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} +declare type A = number +// Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} + opaque type A = AType // Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} @@ -342,6 +353,9 @@ class C implements AType {} interface AType {} // Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} +declare interface A {} +// Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} + ({ a: ({b() {}}: AType) }) // Additional rules: {"no-undef":2,"no-use-before-define":[2,"nofunc"]} @@ -968,6 +982,31 @@ var a = 1; var b = 1; type f = { get(key: a): string, get(key: b): string } + +### no-flow-fix-me-comments + +Disallows `$FlowFixMe` comment suppressions. + +This is especially useful as a warning to ensure instances of `$FlowFixMe` in your codebase get fixed over time. + + +#### Options + +This rule takes an optional RegExp that comments a text RegExp that makes the supression valid. + +```js +{ + "rules": { + "flowtype/no-flow-fix-me-comments": [ + 1, + "TODO\s+[0-9]+" + ] + } +} +``` + + + ### no-mutable-array @@ -1500,6 +1539,95 @@ type Foo = { a: Foo, b: Bar } + +### require-exact-type + +This rule enforces [exact object types](https://flow.org/en/docs/types/objects/#toc-exact-object-types). + + +#### Options + +The rule has one string option: + +* `"always"` (default): Report all object type definitions that aren't exact. +* `"never"`: Report all object type definitions that are exact. + +```js +{ + "rules": { + "flowtype/require-exact-type": [ + 2, + "always" + ] + } +} + +{ + "rules": { + "flowtype/require-exact-type": [ + 2, + "never" + ] + } +} +``` + +The following patterns are considered problems: + +```js +type foo = {}; +// Message: Type identifier 'foo' must be exact. + +type foo = { bar: string }; +// Message: Type identifier 'foo' must be exact. + +// Options: ["always"] +type foo = {}; +// Message: Type identifier 'foo' must be exact. + +// Options: ["always"] +type foo = { bar: string }; +// Message: Type identifier 'foo' must be exact. + +// Options: ["never"] +type foo = {| |}; +// Message: Type identifier 'foo' must not be exact. + +// Options: ["never"] +type foo = {| bar: string |}; +// Message: Type identifier 'foo' must not be exact. +``` + +The following patterns are not considered problems: + +```js +type foo = {| |}; + +type foo = {| bar: string |}; + +type foo = number; + +// Options: ["always"] +type foo = {| |}; + +// Options: ["always"] +type foo = {| bar: string |}; + +// Options: ["always"] +type foo = number; + +// Options: ["never"] +type foo = { }; + +// Options: ["never"] +type foo = { bar: string }; + +// Options: ["never"] +type foo = number; +``` + + + ### require-parameter-type @@ -1789,6 +1917,19 @@ async () => {} async function x() {} // Message: Missing return type annotation. +// Options: ["always",{"annotateUndefined":"always"}] +class Test { constructor() { } } +// Message: Must annotate undefined return type. + +class Test { foo() { return 42; } } +// Message: Missing return type annotation. + +class Test { foo = () => { return 42; } } +// Message: Missing return type annotation. + +class Test { foo = () => 42; } +// Message: Missing return type annotation. + // Options: ["always"] async () => { return; } // Message: Missing return type annotation. @@ -1869,6 +2010,24 @@ async function doThing(): Promise {} // Options: ["always",{"annotateUndefined":"always"}] function* doThing(): Generator { yield 2; } +// Options: ["always",{"annotateUndefined":"always","excludeMatching":["constructor"]}] +class Test { constructor() { } } + +class Test { constructor() { } } + +// Options: ["always",{"excludeMatching":["foo"]}] +class Test { foo() { return 42; } } + +// Options: ["always",{"excludeMatching":["foo"]}] +class Test { foo = () => { return 42; } } + +// Options: ["always",{"excludeMatching":["foo"]}] +class Test { foo = () => 42; } + +class Test { foo = (): number => { return 42; } } + +class Test { foo = (): number => 42; } + async (foo): Promise => { return 3; } // Options: ["always",{"excludeArrowFunctions":true}] diff --git a/src/index.js b/src/index.js index a1de5e97..5fce4432 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import noTypesMissingFileAnnotation from './rules/noTypesMissingFileAnnotation'; import noUnusedExpressions from './rules/noUnusedExpressions'; import noWeakTypes from './rules/noWeakTypes'; import objectTypeDelimiter from './rules/objectTypeDelimiter'; +import requireExactType from './rules/requireExactType'; import requireParameterType from './rules/requireParameterType'; import requireReturnType from './rules/requireReturnType'; import requireValidFileAnnotation from './rules/requireValidFileAnnotation'; @@ -40,6 +41,7 @@ const rules = { 'no-unused-expressions': noUnusedExpressions, 'no-weak-types': noWeakTypes, 'object-type-delimiter': objectTypeDelimiter, + 'require-exact-type': requireExactType, 'require-parameter-type': requireParameterType, 'require-return-type': requireReturnType, 'require-valid-file-annotation': requireValidFileAnnotation, @@ -79,6 +81,7 @@ export default { 'no-mutable-array': 0, 'no-weak-types': 0, 'object-type-delimiter': 0, + 'require-exact-type': 0, 'require-parameter-type': 0, 'require-return-type': 0, 'require-variable-type': 0, diff --git a/src/rules/requireExactType.js b/src/rules/requireExactType.js new file mode 100644 index 00000000..f480f037 --- /dev/null +++ b/src/rules/requireExactType.js @@ -0,0 +1,39 @@ +const schema = [ + { + enum: ['always', 'never'], + type: 'string' + } +]; + +const create = (context) => { + const always = (context.options[0] || 'always') === 'always'; + + return { + TypeAlias (node) { + const {id: {name}, right: {type, exact}} = node; + + if (type === 'ObjectTypeAnnotation') { + if (always && !exact) { + context.report({ + data: {name}, + message: 'Type identifier \'{{name}}\' must be exact.', + node + }); + } + + if (!always && exact) { + context.report({ + data: {name}, + message: 'Type identifier \'{{name}}\' must not be exact.', + node + }); + } + } + } + }; +}; + +export default { + create, + schema +}; diff --git a/tests/rules/assertions/requireExactType.js b/tests/rules/assertions/requireExactType.js new file mode 100644 index 00000000..92dab7c7 --- /dev/null +++ b/tests/rules/assertions/requireExactType.js @@ -0,0 +1,103 @@ +export default { + invalid: [ + + // Always + + { + code: 'type foo = {};', + errors: [ + { + message: 'Type identifier \'foo\' must be exact.' + } + ] + }, + { + code: 'type foo = { bar: string };', + errors: [ + { + message: 'Type identifier \'foo\' must be exact.' + } + ] + }, + { + code: 'type foo = {};', + errors: [ + { + message: 'Type identifier \'foo\' must be exact.' + } + ], + options: ['always'] + }, + { + code: 'type foo = { bar: string };', + errors: [ + { + message: 'Type identifier \'foo\' must be exact.' + } + ], + options: ['always'] + }, + + // Never + + { + code: 'type foo = {| |};', + errors: [ + { + message: 'Type identifier \'foo\' must not be exact.' + } + ], + options: ['never'] + }, + { + code: 'type foo = {| bar: string |};', + errors: [ + { + message: 'Type identifier \'foo\' must not be exact.' + } + ], + options: ['never'] + } + ], + valid: [ + + // Always + + { + code: 'type foo = {| |};' + }, + { + code: 'type foo = {| bar: string |};' + }, + { + code: 'type foo = number;' + }, + { + code: 'type foo = {| |};', + options: ['always'] + }, + { + code: 'type foo = {| bar: string |};', + options: ['always'] + }, + { + code: 'type foo = number;', + options: ['always'] + }, + + // Never + + { + code: 'type foo = { };', + options: ['never'] + }, + { + code: 'type foo = { bar: string };', + options: ['never'] + }, + { + code: 'type foo = number;', + options: ['never'] + } + ] +}; diff --git a/tests/rules/index.js b/tests/rules/index.js index 94ae8f03..eaf513b0 100644 --- a/tests/rules/index.js +++ b/tests/rules/index.js @@ -23,6 +23,7 @@ const reportingRules = [ 'no-unused-expressions', 'no-weak-types', 'object-type-delimiter', + 'require-exact-type', 'require-parameter-type', 'require-return-type', 'require-valid-file-annotation',