diff --git a/.eslintrc b/.eslintrc index 4087d78..53c692d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,8 +1,13 @@ { "extends": ["eslint-config-bzc"], + "parser": "hermes-eslint", "rules": { "global-require": 0, "import/no-dynamic-require": 0, - "no-restricted-syntax": 0 + "no-restricted-syntax": 0, + "ft-flow/define-flow-type": 0, + "ft-flow/use-flow-type": 0, + "no-undef": 0, + "no-use-before-define": 0 } } diff --git a/.flowconfig b/.flowconfig index 49cd897..9d485f1 100644 --- a/.flowconfig +++ b/.flowconfig @@ -8,6 +8,9 @@ !.*node_modules/tiny-invariant/.* !.*node_modules/key-commander/.* !.*node_modules/react-ld/.* +!.*node_modules/hermes-eslint/.* +!.*node_modules/hermes-estree/.* +!.*node_modules/hermes-parser/.* [include] @@ -18,8 +21,17 @@ [lints] deprecated-type=error deprecated-utility=error +untyped-type-import=error +unclear-type=error unnecessary-optional-chain=error -ambiguous-object-type=error + +; ES6 module lints +invalid-import-star-use=error +non-const-var-export=error +this-in-exported-function=error +mixed-import-and-require=error +export-renamed-default=error +default-import-access=error [options] enums=true @@ -28,7 +40,13 @@ format.single_quotes=true exact_by_default=true include_warnings=true +# this is a dirty hack to allow us to declare "libs" that reference non "libs" types +module.name_mapper='^eslint$' -> '/flow-typed-local/ESLint.js.flow' + [strict] +nonstrict-import +sketchy-null +untyped-import [version] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c961a85 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "javascript.validate.enable": false, + "flow.enabled": true +} diff --git a/flow-typed-local/ESLint.js.flow b/flow-typed-local/ESLint.js.flow new file mode 100644 index 0000000..3ecee02 --- /dev/null +++ b/flow-typed-local/ESLint.js.flow @@ -0,0 +1,740 @@ +// @flow strict-local +/** + * + * IMPORTANT NOTE + * + * This file intentionally uses interfaces and `+` for readonly. + * + * - $ReadOnly is an "evaluated type" in flow, meaning it's not completely optimised. + * So we instead annotate every property with `+` manually. + * + * - You cannot preserve property readonly-ness via `...Type` spreads. + * Extending an interface, on the other hand, does preserve readonlyness of properties. + * + * Please ensure all properties are marked as readonly! + */ + +/* eslint-disable ft-flow/type-id-match */ + +import type { Scope, ScopeManager, Variable } from 'hermes-eslint'; +import typeof { VisitorKeys } from 'hermes-eslint'; +import type { + Comment, + ESNode, + ESQueryNodeSelectors, + ESQueryNodeSelectorsWithoutFallback, + Position, + Program, + Range, + SourceLocation, + Token, +} from 'hermes-estree'; + +export type { + Definition, + Reference, + Scope, + ScopeManager, + Variable, + VisitorKeys, +} from 'hermes-eslint'; + +export type SourceCode = { + +text: string, + +ast: Program, + +lines: $ReadOnlyArray, + +hasBOM: boolean, + +parserServices: ParserServices, + +scopeManager: ScopeManager, + +visitorKeys: VisitorKeys, + + // static splitLines(text: string): $ReadOnlyArray; + + +getText: ( + node?: ESNode | Comment, + beforeCount?: number, + afterCount?: number + ) => string, + + +getLines: () => $ReadOnlyArray, + + +getAllComments: () => $ReadOnlyArray, + + +getComments: (node: ESNode) => { + +leading: $ReadOnlyArray, + +trailing: $ReadOnlyArray, + }, + + +getJSDocComment: (node: ESNode) => ?Comment, + + +getNodeByRangeIndex: (index: number) => ?ESNode, + + +isSpaceBetweenTokens: (first: Token, second: Token) => boolean, + + +getLocFromIndex: (index: number) => Position, + + +getIndexFromLoc: (location: Position) => number, + + // Inherited methods from TokenStore + // --------------------------------- + + +getTokenByRangeStart: ( + offset: number, + options?: { + +includeComments?: boolean, + } + ) => ?Token, + + +getFirstToken: (node: ESNode, options?: CursorWithSkipOptions) => ?Token, + + +getFirstTokens: ( + node: ESNode, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getLastToken: (node: ESNode, options?: CursorWithSkipOptions) => ?Token, + + +getLastTokens: ( + node: ESNode, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getTokenBefore: ( + node: ESNode | Token | Comment, + options?: CursorWithSkipOptions + ) => ?Token, + + +getTokensBefore: ( + node: ESNode | Token | Comment, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getTokenAfter: ( + node: ESNode | Token | Comment, + options?: CursorWithSkipOptions + ) => ?Token, + + +getTokensAfter: ( + node: ESNode | Token | Comment, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getFirstTokenBetween: ( + left: ESNode | Token | Comment, + right: ESNode | Token | Comment, + options?: CursorWithSkipOptions + ) => ?Token, + + +getFirstTokensBetween: ( + left: ESNode | Token | Comment, + right: ESNode | Token | Comment, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getLastTokenBetween: ( + left: ESNode | Token | Comment, + right: ESNode | Token | Comment, + options?: CursorWithSkipOptions + ) => ?Token, + + +getLastTokensBetween: ( + left: ESNode | Token | Comment, + right: ESNode | Token | Comment, + options?: CursorWithCountOptions + ) => $ReadOnlyArray, + + +getTokensBetween: ( + left: ESNode | Token | Comment, + right: ESNode | Token | Comment, + padding?: number | FilterPredicate | CursorWithCountOptions + ) => $ReadOnlyArray, + + +getTokens: ( + node: ESNode, + beforeCount?: number, + afterCount?: number + ) => $ReadOnlyArray, + +getTokens: ( + node: ESNode, + options?: FilterPredicate | CursorWithCountOptions + ) => $ReadOnlyArray, + + +commentsExistBetween: ( + left: ESNode | Token, + right: ESNode | Token + ) => boolean, + + +getCommentsBefore: (nodeOrToken: ESNode | Token) => $ReadOnlyArray, + + +getCommentsAfter: (nodeOrToken: ESNode | Token) => $ReadOnlyArray, + + +getCommentsInside: (node: ESNode) => $ReadOnlyArray, +}; + +// flow parser provides no services +type ParserServices = void; + +type FilterPredicate = (tokenOrComment: Token | Comment) => boolean; + +type CursorWithSkipOptions = + | number + | FilterPredicate + | { + +includeComments?: boolean, + +filter?: FilterPredicate, + +skip?: number, + }; + +type CursorWithCountOptions = + | number + | FilterPredicate + | { + +includeComments?: boolean, + +filter?: FilterPredicate, + +count?: number, + }; + +/// / Rule + +type RuleCreateFunction = []> = ( + context: RuleContext +) => RuleListener; + +export type RuleModule = []> = { + +create: RuleCreateFunction, + +meta?: RuleMetaData, +}; + +export type NodeListenerWithoutFallbackIndexer = + ESQueryNodeSelectorsWithoutFallback; + +export type NodeListener = ESQueryNodeSelectors; + +export type RuleListener = { + ...$ReadOnly, + + +onCodePathStart?: (codePath: CodePath, node: ESNode) => void, + + +onCodePathEnd?: (codePath: CodePath, node: ESNode) => void, + + +onCodePathSegmentStart?: (segment: CodePathSegment, node: ESNode) => void, + + +onCodePathSegmentEnd?: (segment: CodePathSegment, node: ESNode) => void, + + +onCodePathSegmentLoop?: ( + fromSegment: CodePathSegment, + toSegment: CodePathSegment, + node: ESNode + ) => void, +}; + +export type CodePath = { + +id: string, + +initialSegment: CodePathSegment, + +finalSegments: $ReadOnlyArray, + +returnedSegments: $ReadOnlyArray, + +thrownSegments: $ReadOnlyArray, + +currentSegments: $ReadOnlyArray, + +upper: ?CodePath, + +childCodePaths: $ReadOnlyArray, +}; + +type CodePathSegment = { + +id: string, + +nextSegments: $ReadOnlyArray, + +prevSegments: $ReadOnlyArray, + +reachable: boolean, +}; + +export type RuleFixType = 'problem' | 'suggestion' | 'layout' | 'directive'; + +export type RuleMetaData = { + +docs?: { + /** provides the short description of the rule in the [rules index](https://org/docs/rules/) */ + +description?: string, + /** specifies the heading under which the rule is listed in the [rules index](https://org/docs/rules/) */ + +category?: string, + /** is whether the `"extends": "eslint:recommended"` property in a [configuration file](https://org/docs/user-guide/configuring#extending-configuration-files) enables the rule */ + +recommended?: boolean, + /** specifies the URL at which the full documentation can be accessed */ + +url?: string, + /** @deprecated - use meta.hasSuggestions instead */ + // +suggestion?: boolean, + }, + +messages?: { + +[messageId: string]: string, + }, + +fixable?: 'code' | 'whitespace', + +hasSuggestions?: boolean, + // TODO - we could probably strictly type this + +schema?: $ReadOnly<{ ... }> | $ReadOnlyArray<$ReadOnly<{ ... }>>, + +deprecated?: boolean, + +type?: RuleFixType, +}; + +export type RuleContext = []> = { + +id: string, + +options: TOptions, + +settings: { + +[name: string]: mixed, + }, + +parserPath: string, + +parserOptions: ParserOptions, + +parserServices: ParserServices, + + +getAncestors: () => $ReadOnlyArray, + + +getDeclaredVariables: (node: ESNode) => $ReadOnlyArray, + + +getFilename: () => string, + + +getScope: () => Scope, + + +getSourceCode: () => SourceCode, + + +markVariableAsUsed: (name: string) => boolean, + + +report: (descriptor: ReportDescriptor) => void, +}; + +type ReportDescriptorOptionsBase = { + +data?: { + +[key: string]: string | number, + }, + +fix?: ?(fixer: RuleFixer) => ?( + | Fix + // iterable because ESLint support generators + | $Iterable + ), +}; + +export type SuggestionReportDescriptor = { + ...$ReadOnly, + +messageId: string, + // we don't want people to use this - instead they should use messageId + // it's better and easier to test and enforces placeholder usage as well + // +desc: string +}; + +type ReportDescriptorOptions = { + ...$ReadOnly, + +suggest?: ?$ReadOnlyArray, +}; + +type ReportDescriptorLocationNode = { + +node: ESNode | Comment | Token, +}; + +type ReportDescriptorLocationLoc = { + +loc: SourceLocation | Position, +}; + +export type ReportDescriptor = + | { + +messageId: string, + ...$ReadOnly, + ...$ReadOnly, + } + | { + +messageId: string, + ...$ReadOnly, + ...$ReadOnly, + }; + +// we don't want people to use this - instead they should use messageId +// it's better and easier to test and enforces placeholder usage as well +// +message: string + +export type RuleFixer = { + +insertTextAfter: (nodeOrToken: ESNode | Token, text: string) => Fix, + + +insertTextAfterRange: (range: Range, text: string) => Fix, + + +insertTextBefore: (nodeOrToken: ESNode | Token, text: string) => Fix, + + +insertTextBeforeRange: (range: Range, text: string) => Fix, + + +remove: (nodeOrToken: ESNode | Token) => Fix, + + +removeRange: (range: Range) => Fix, + + +replaceText: (nodeOrToken: ESNode | Token, text: string) => Fix, + + +replaceTextRange: (range: Range, text: string) => Fix, +}; + +export type Fix = { + +range: Range, + +text: string, +}; + +/// / Linter + +declare export class Linter { + static +version: string, + + +version: string, + + constructor(options?: { + +cwd?: string, + }): Linter, + + verify( + code: SourceCode | string, + config: LinterConfig<>, + filename?: string + ): $ReadOnlyArray, + verify( + code: SourceCode | string, + config: LinterConfig<>, + options: LintOptions + ): $ReadOnlyArray, + + verifyAndFix( + code: string, + config: LinterConfig<>, + filename?: string + ): FixReport, + verifyAndFix( + code: string, + config: LinterConfig<>, + options: FixOptions + ): FixReport, + + getSourceCode(): SourceCode, + + defineRule = []>( + name: string, + rule: RuleModule + ): void, + + defineRules(rules: { + +[name: string]: RuleModule<>, + }): void, + + getRules(): $ReadOnlyMap>, + + defineParser(name: string, parser: ParserModule): void, +} + +type Severity = 0 | 1 | 2; + +type RuleLevel = Severity | 'off' | 'warn' | 'error'; + +// flow doesn't support variadic tuples +// type RuleLevelAndOptions = [RuleLevel, ...mixed]; +type RuleLevelAndOptions = $ReadOnlyArray; + +type RuleEntry = RuleLevel | RuleLevelAndOptions; + +export type RulesRecord = { + +[rule: string]: RuleEntry, +}; + +type HasRules = { + +rules?: $Partial, +}; + +type BaseConfig = { + ...$ReadOnly>, + +$schema?: string, + +env?: { [name: string]: boolean }, + +extends?: string | $ReadOnlyArray, + +globals?: { + [name: string]: boolean | 'readonly' | 'readable' | 'writable' | 'writable', + }, + +noInlineConfig?: boolean, + +overrides?: $ReadOnlyArray>, + +parser?: string, + +parserOptions?: ParserOptions, + +plugins?: $ReadOnlyArray, + +processor?: string, + +reportUnusedDisableDirectives?: boolean, + +settings?: { [name: string]: mixed }, +}; + +type ConfigOverride = { + ...$ReadOnly>, + +excludedFiles?: string | $ReadOnlyArray, + +files: string | $ReadOnlyArray, +}; + +// https://github.com/eslint/eslint/blob/v6.8.0/conf/config-schema.js +export type LinterConfig = { + ...$ReadOnly>, + +ignorePatterns?: string | $ReadOnlyArray, + +root?: boolean, +}; + +export type ParserOptions = { + +ecmaVersion?: | 3 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 2015 + | 2016 + | 2017 + | 2018 + | 2019 + | 2020, + +sourceType?: 'script' | 'module', + +ecmaFeatures?: { + +globalReturn?: boolean, + +impliedStrict?: boolean, + +jsx?: boolean, + +experimentalObjectRestSpread?: boolean, + +[key: string]: mixed, + }, + +[key: string]: mixed, +}; + +type LintOptions = { + +filename?: string, + +preprocess?: (code: string) => $ReadOnlyArray, + +postprocess?: ( + problemLists: $ReadOnlyArray + ) => $ReadOnlyArray, + +filterCodeBlock?: boolean, + +disableFixes?: boolean, + +allowInlineConfig?: boolean, + +reportUnusedDisableDirectives?: boolean, +}; + +type LintSuggestion = { + +desc: string, + +fix: Fix, + +messageId?: string, +}; + +type FatalLintMessage = { + +column: number, + +line: number, + +endColumn?: void, + +endLine?: void, + +ruleId: null, + +message: string, + +fatal: true, + +severity: Severity, +}; + +type GoodLintMessage = { + +column: number, + +line: number, + +endColumn?: number, + +endLine?: number, + +ruleId: string, + +message: string, + +messageId?: string, + +nodeType?: string, + +fatal?: void, + +severity: Severity, + +fix?: Fix, + +suggestions?: $ReadOnlyArray, +}; + +export type LintMessage = FatalLintMessage | GoodLintMessage; + +type FixOptions = { + ...$ReadOnly, + +fix?: boolean, +}; + +type FixReport = { + +fixed: boolean, + +output: string, + +messages: $ReadOnlyArray, +}; + +type ParserModule = + | { + parse(text: string, options?: mixed): Program, + } + | { + parseForESLint(text: string, options?: mixed): ESLintParseResult, + }; + +type ESLintParseResult = { + +ast: Program, + +parserServices?: ParserServices, + +scopeManager?: ScopeManager, + +visitorKeys?: VisitorKeys, +}; + +/// / ESLint + +declare export class ESLint { + static +version: string, + + static outputFixes(results: $ReadOnlyArray): Promise, + + static getErrorResults( + results: $ReadOnlyArray + ): $ReadOnlyArray, + + constructor(options: ESLintOptions): ESLint, + + lintFiles( + patterns: string | $ReadOnlyArray + ): Promise<$ReadOnlyArray>, + + lintText( + code: string, + options?: { + +filePath?: string, + +warnIgnored?: boolean, + } + ): Promise<$ReadOnlyArray>, + + calculateConfigForFile(filePath: string): Promise>, + + isPathIgnored(filePath: string): Promise, + + loadFormatter(nameOrPath?: string): Promise, +} + +export type ESLintOptions = { + // File enumeration + +cwd?: string, + +errorOnUnmatchedPattern?: boolean, + +extensions?: $ReadOnlyArray, + +globInputPaths?: boolean, + +ignore?: boolean, + +ignorePath?: string, + + // Linting + +allowInlineConfig?: boolean, + +baseConfig?: LinterConfig<>, + +overrideConfig?: LinterConfig<>, + +overrideConfigFile?: string, + +plugins?: { + +[name: string]: mixed, + }, + +reportUnusedDisableDirectives?: RuleLevel, + +resolvePluginsRelativeTo?: string, + +rulePaths?: $ReadOnlyArray, + +useEslintrc?: boolean, + + // Autofix + +fix?: boolean | ((message: LintMessage) => boolean), + +fixTypes?: $ReadOnlyArray, + + // Cache-related + +cache?: boolean, + +cacheLocation?: string, + +cacheStrategy?: 'content' | 'metadata', +}; + +export type LintResult = { + +filePath: string, + +messages: $ReadOnlyArray, + +errorCount: number, + +warningCount: number, + +fixableErrorCount: number, + +fixableWarningCount: number, + +output?: string, + +source?: string, + +usedDeprecatedRules: $ReadOnlyArray, +}; + +type LintResultData = { + +rulesMeta: { + +[ruleId: string]: RuleMetaData, + }, +}; + +type DeprecatedRuleUse = { + +ruleId: string, + +replacedBy: $ReadOnlyArray, +}; + +type Formatter = { + +format: ( + results: $ReadOnlyArray, + data?: LintResultData + ) => string, +}; + +/// / RuleTester + +export type RuleTesterTests = []> = { + +valid?: $ReadOnlyArray>, + +invalid?: $ReadOnlyArray>, +}; + +declare export class RuleTester { + constructor(config?: BaseConfig<>): RuleTester, + + run = []>( + name: string, + rule: RuleModule, + tests: RuleTesterTests + ): void, + + linter: Linter, + + static setDefaultConfig(config: BaseConfig<>): void, + + static describe: (text: string, callback: () => void) => void, + static it: (text: string, callback: () => void) => void, +} + +export type ValidTestCase = []> = { + +code: string, + +options?: TOptions, + +filename?: string, + +parserOptions?: ParserOptions, + +settings?: { + +[name: string]: mixed, + }, + +parser?: string, + +globals?: { + +[name: string]: boolean, + }, +}; + +type SuggestionOutput = { + // we don't want people to use this - instead they should use messageId + // it's better and easier to test and enforces placeholder usage as well + // +desc?: string; + +messageId?: string, + +data?: { + +[key: string]: mixed, + }, + +output: string, +}; + +export type InvalidTestCase = []> = { + ...$ReadOnly>, + + // we don't allow the `errors: 1` form because it's lazy - the test assserts + // nothing useful and promotes not expecting exact placeholders and such + // it will also match exceptions, which is obviously undesirable. + // + // we don't allow the errors: ['message'] from because we want people to use + // messageIds as they are easier to manage and are resilient to wording changes + +errors: $ReadOnlyArray, + +output?: string | null, + // can be used to force a test.only() run - i.e. skipping all other tests + +only?: boolean, +}; + +export type TestCaseError = { + // we don't want people to use this - instead they should use messageId + // it's better and easier to test and enforces placeholder usage as well + // +message?: string | RegExp; + + // we enforce the messageId exists so people don't get lazy and do + // `errors: [{}]` or `errors: [{ruleId: 'my-rule'}]` + +messageId: string, + +type?: string, + +data?: mixed, + +line?: number, + +column?: number, + +endLine?: number, + +endColumn?: number, + +suggestions?: ?$ReadOnlyArray, +}; diff --git a/flow-typed/npm/eslint_vx.x.x.js b/flow-typed/npm/eslint_vx.x.x.js deleted file mode 100644 index e32b2a1..0000000 --- a/flow-typed/npm/eslint_vx.x.x.js +++ /dev/null @@ -1,44 +0,0 @@ -// flow-typed signature: 65c82ab5e500f8a48ae94acae911f339 -// flow-typed version: <>/eslint_v^8.1.0/flow_v0.167.1 - -/** - * This is an autogenerated libdef stub for: - * - * 'eslint' - * - * Fill this stub out by replacing all the `any` types. - * - * Once filled out, we encourage you to share your work with the - * community by sending a pull request to: - * https://github.com/flowtype/flow-typed - */ - -declare module 'eslint' { - declare type Rule$Context = {| - report: ({ ... }) => void, - getAllComments: () => Array<{| - type: string, - value: string, - |}>, - getSourceCode: () => Rule$Context, - |}; - - declare type Rule$Create = (context: Rule$Context) => {| - [key: string]: any, - |}; - - declare class RuleTester { - constructor(config?: {| - [key: string]: any, - |}): this; - - static describe(title: string, fn: () => void): void; - static it(title: string, fn: () => void): void; - - run(...args: Array): this; - } - - declare module.exports: {| - RuleTester: typeof RuleTester, - |}; -} diff --git a/package.json b/package.json index 00cb467..3db5730 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "prepublishOnly": "yarn create-readme && yarn build" }, "dependencies": { + "hermes-eslint": "^0.9.0", + "hermes-estree": "^0.9.0", "lodash": "^4.17.21", "string-natural-compare": "^3.0.1" }, diff --git a/src/rules/noFlowSuppressionsInStrictFiles.js b/src/rules/noFlowSuppressionsInStrictFiles.js index 6b52c5a..abdd8df 100644 --- a/src/rules/noFlowSuppressionsInStrictFiles.js +++ b/src/rules/noFlowSuppressionsInStrictFiles.js @@ -1,65 +1,74 @@ -// @flow -import _ from 'lodash'; -import type { Rule$Create } from 'eslint'; +// @flow strict-local +import type { RuleModule } from 'eslint'; import { suppressionTypes } from '../utilities'; const FLOW_STRICT_MATCHER = /^\s*@(?:no)?flow\s*strict(?:-local)?\s*$/u; -const isStrictFlowFile = (context) => context - .getAllComments() - .some((comment) => FLOW_STRICT_MATCHER.test(comment.value)); - -const message = 'No suppression comments are allowed in "strict" Flow files. Either remove the error suppression, or lower the strictness of this module.'; - -const schema = [ - { - additionalProperties: false, - properties: ({}: { [key: string]: {| type: 'boolean' |}}), - type: 'object', +export default ({ + meta: { + messages: { + noFlowSuppression: + 'No suppression comments are allowed in "strict" Flow files. Either remove the error suppression, or lower the strictness of this module.', + }, + schema: [ + { + additionalProperties: false, + properties: Object.fromEntries( + suppressionTypes.map((suppressionType) => [ + suppressionType, + { + type: 'boolean', + }, + ]), + ), + type: 'object', + }, + ], }, -]; - -suppressionTypes.forEach((o) => { - schema[0].properties[o] = { - type: 'boolean', - }; -}); + create(context) { + const suppressionOptions = context.options[0] ?? {}; -const create: Rule$Create = (context) => { - const suppressionOptions = _.get(context, 'options[0]', {}); + const isStrictFlowFile = () => context + .getSourceCode() + .getAllComments() + .some((comment) => FLOW_STRICT_MATCHER.test(comment.value)); + if (!isStrictFlowFile()) { + // Skip this file - nothing to check here + return {}; + } - if (!isStrictFlowFile(context)) { - // Skip this file - nothing to check here - return {}; - } + return { + Program: () => { + const comments = context + .getSourceCode() + .getAllComments() + .filter((node) => node.type === 'Block' || node.type === 'Line'); - return { - Program: () => { - const comments = context - .getSourceCode() - .getAllComments() - .filter((node) => node.type === 'Block' || node.type === 'Line'); + for (const commentNode of comments) { + const comment = commentNode.value.trimStart(); + const match = suppressionTypes.some((prefix) => { + if (suppressionOptions[prefix] === false) return false; - for (const commentNode of comments) { - const comment = commentNode.value.trimStart(); - const match = suppressionTypes.some((prefix) => { - if (suppressionOptions[prefix] === false) return false; - - return comment.startsWith(prefix); - }); - if (match) { - context.report({ - message, - node: commentNode, + return comment.startsWith(prefix); }); + if (match) { + context.report({ + messageId: 'noFlowSuppression', + node: commentNode, + }); + } } - } - }, - }; -}; - -export default { - create, - schema, -}; + }, + }; + }, +}: RuleModule< + [ + { + $FlowExpectedError?: boolean, + $FlowFixMe?: boolean, + $FlowIgnore?: boolean, + $FlowIssue?: boolean, + } | void, + ] +>); diff --git a/src/rules/noWeakTypes.js b/src/rules/noWeakTypes.js index 851066f..039f2d5 100644 --- a/src/rules/noWeakTypes.js +++ b/src/rules/noWeakTypes.js @@ -1,81 +1,91 @@ -// @flow -import type { Rule$Create } from 'eslint'; -import _ from 'lodash'; +// @flow strict-local +import type { RuleListener, RuleModule } from 'eslint'; -const schema = [ +export type OptionsT = [ { - additionalProperties: false, - properties: { - any: { - type: 'boolean', - }, - Function: { - type: 'boolean', - }, - Object: { - type: 'boolean', - }, - suppressTypes: { - items: { - type: 'string', - }, - type: 'array', - }, - }, - type: 'object', - }, + any?: boolean, + Function?: boolean, + Object?: boolean, + suppressTypes?: $ReadOnlyArray, + [string]: boolean, + } | void, ]; -const reportWeakType = (context, weakType, custom = false) => (node) => { - context.report({ - data: { weakType }, - message: `Unexpected use of${custom ? ' custom' : ''} weak type "{{weakType}}"`, - node, - }); -}; - -const genericTypeEvaluator = ( - context, - { - checkFunction, - checkObject, - suppressTypes, +export default ({ + meta: { + messages: { + noWeakTypes: 'Unexpected use of weak type "{{weakType}}"', + noCustomWeakTypes: 'Unexpected use of custom weak type "{{weakType}}"', + }, + schema: [ + { + additionalProperties: false, + properties: { + any: { + type: 'boolean', + }, + Function: { + type: 'boolean', + }, + Object: { + type: 'boolean', + }, + suppressTypes: { + items: { + type: 'string', + }, + type: 'array', + }, + }, + type: 'object', + }, + ], }, -) => (node) => { - const name = _.get(node, 'id.name'); + create(context) { + const checkAny = context.options[0]?.any === true; + const checkFunction = context.options[0]?.Function === true; + const checkObject = context.options[0]?.Object === true; + const suppressTypes = context.options[0]?.suppressTypes ?? []; - if ((checkFunction && name === 'Function') || (checkObject && name === 'Object')) { - reportWeakType(context, name)(node); - } - if (suppressTypes.includes(name)) { - reportWeakType(context, name, true)(node); - } -}; + const checks: RuleListener = {}; -const create: Rule$Create = (context) => { - const checkAny = _.get(context, 'options[0].any', true) === true; - const checkFunction = _.get(context, 'options[0].Function', true) === true; - const checkObject = _.get(context, 'options[0].Object', true) === true; - const suppressTypes = _.get(context, 'options[0].suppressTypes', []); + if (checkAny) { + checks.AnyTypeAnnotation = (node) => { + context.report({ + data: { style: '', weakType: 'any' }, + messageId: 'noWeakType', + node, + }); + }; + } - const checks = {}; + if (checkFunction || checkObject || suppressTypes.length > 0) { + checks.GenericTypeAnnotation = (node) => { + if (node.id.type === 'QualifiedTypeIdentifier') { + return; + } + const { name } = node.id; - if (checkAny) { - checks.AnyTypeAnnotation = reportWeakType(context, 'any'); - } + if ( + (checkFunction && name === 'Function') + || (checkObject && name === 'Object') + ) { + context.report({ + data: { weakType: name }, + messageId: 'noWeakType', + node, + }); + } + if (suppressTypes.includes(name)) { + context.report({ + data: { weakType: name }, + messageId: 'noCustomWeakType', + node, + }); + } + }; + } - if (checkFunction || checkObject || suppressTypes.length > 0) { - checks.GenericTypeAnnotation = genericTypeEvaluator(context, { - checkFunction, - checkObject, - suppressTypes, - }); - } - - return checks; -}; - -export default { - create, - schema, -}; + return checks; + }, +}: RuleModule); diff --git a/tests/rules/assertions/noWeakTypes.js b/tests/rules/assertions/noWeakTypes.js index 4d6f277..533cc75 100644 --- a/tests/rules/assertions/noWeakTypes.js +++ b/tests/rules/assertions/noWeakTypes.js @@ -1,210 +1,330 @@ -export default { +// @flow strict-local +import type { OptionsT } from '../../../src/rules/noWeakTypes'; +import type { RuleTestAssertionsT } from '../types'; + +export default ({ invalid: [ { code: 'function foo(thing): any {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'function foo(thing): Promise {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'function foo(thing): Promise> {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'function foo(thing): Object {}', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'function foo(thing): Promise {}', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'function foo(thing): Promise> {}', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'function foo(thing): Function {}', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: 'function foo(thing): Promise {}', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: 'function foo(thing): Promise> {}', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: '(foo: any) => {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: '(foo: Function) => {}', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: '(foo?: any) => {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: '(foo?: Function) => {}', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: '(foo: { a: any }) => {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: '(foo: { a: Object }) => {}', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: '(foo: any[]) => {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'type Foo = any', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'type Foo = Function', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: 'type Foo = { a: any }', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'type Foo = { a: Object }', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'type Foo = { (a: Object): string }', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'type Foo = { (a: string): Function }', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: 'function foo(thing: any) {}', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'function foo(thing: Object) {}', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'var foo: Function', - errors: [{ - message: 'Unexpected use of weak type "Function"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Function' }, + }, + ], }, { code: 'var foo: Object', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'class Foo { props: any }', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'class Foo { props: Object }', - errors: [{ - message: 'Unexpected use of weak type "Object"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], }, { code: 'var foo: any', - errors: [{ - message: 'Unexpected use of weak type "any"', - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + ], }, { code: 'type X = any; type Y = Function; type Z = Object', errors: [ - { message: 'Unexpected use of weak type "any"' }, - { message: 'Unexpected use of weak type "Object"' }, + { + messageId: 'noWeakTypes', + data: { weakType: 'any' }, + }, + { + messageId: 'noWeakTypes', + data: { weakType: 'Object' }, + }, + ], + options: [ + { + Function: false, + }, ], - options: [{ - Function: false, - }], }, { code: 'type X = any; type Y = Function; type Z = Object', - errors: [{ message: 'Unexpected use of weak type "Function"' }], - options: [{ - any: false, - Object: false, - }], + errors: [ + { + messageId: 'noWeakTypes', + data: { weakType: [{ message: 'Function' }] }, + }, + ], + options: [ + { + any: false, + Object: false, + }, + ], }, { code: 'const a: $FlowFixMe = 1', - errors: [{ message: 'Unexpected use of custom weak type "$FlowFixMe"' }], - options: [{ - suppressTypes: ['$FlowFixMe'], - }], + errors: [ + { + messageId: 'noCustomWeakTypes', + data: { weakType: '$FlowFixMe' }, + }, + ], + options: [ + { + suppressTypes: ['$FlowFixMe'], + }, + ], }, { code: 'const a: Something = 1', - errors: [{ message: 'Unexpected use of custom weak type "Something"' }], - options: [{ - suppressTypes: ['$FlowFixMe', 'Something'], - }], + errors: [ + { + messageId: 'noCustomWeakTypes', + data: { weakType: 'Something' }, + }, + ], + options: [ + { + suppressTypes: ['$FlowFixMe', 'Something'], + }, + ], }, ], misconfigured: [ @@ -264,7 +384,12 @@ export default { schemaPath: '#/items/0/properties/Object/type', }, ], - options: [{ Object: 'irrelevant' }], + options: [ + // $FlowIgnore[incompatible-cast] - intentionally bad schema + { + Object: 'irrelevant', + }, + ], }, ], valid: [ @@ -312,10 +437,12 @@ export default { }, { code: 'type X = any; type Y = Object', - options: [{ - any: false, - Object: false, - }], + options: [ + { + any: false, + Object: false, + }, + ], }, { code: 'type X = Function', @@ -331,15 +458,19 @@ export default { }, { code: '// $FlowFixMe\nconst a: string = 1', - options: [{ - suppressTypes: ['$FlowFixMe'], - }], + options: [ + { + suppressTypes: ['$FlowFixMe'], + }, + ], }, { code: 'const Foo = 1', - options: [{ - suppressTypes: ['Foo'], - }], + options: [ + { + suppressTypes: ['Foo'], + }, + ], }, ], -}; +}: RuleTestAssertionsT); diff --git a/tests/rules/index.js b/tests/rules/index.js index 644875a..ba10a59 100644 --- a/tests/rules/index.js +++ b/tests/rules/index.js @@ -1,4 +1,4 @@ -// @flow +// @flow strict-local import assert from 'assert'; import Ajv from 'ajv'; import { @@ -8,10 +8,12 @@ import { camelCase, } from 'lodash'; +// $FlowIgnore[untyped-import] - TODO import plugin from '../../src'; +import type { RuleTestAssertionsT } from './types'; const ruleTester = new RuleTester({ - parser: require.resolve('@babel/eslint-parser'), + parser: require.resolve('hermes-eslint'), }); const reportingRules = [ @@ -72,10 +74,10 @@ const ajv = new Ajv({ for (const ruleName of reportingRules) { // eslint-disable-next-line global-require, import/no-dynamic-require - const assertions = require(`./assertions/${camelCase(ruleName)}`); + const { misconfigured, ...assertions }: RuleTestAssertionsT<> = require(`./assertions/${camelCase(ruleName)}`); - if (assertions.misconfigured) { - for (const misconfiguration of assertions.misconfigured) { + if (misconfigured) { + for (const misconfiguration of misconfigured) { RuleTester.describe(ruleName, () => { RuleTester.describe('misconfigured', () => { RuleTester.it(JSON.stringify(misconfiguration.options), () => { diff --git a/tests/rules/types.js b/tests/rules/types.js new file mode 100644 index 0000000..66f9166 --- /dev/null +++ b/tests/rules/types.js @@ -0,0 +1,23 @@ +// @flow strict-local +import type { RuleMetaData, RuleTesterTests } from 'eslint'; + +export type RuleTestAssertionsT = []> = + $ReadOnly<{ + invalid: RuleTesterTests['invalid'], + misconfigured: $ReadOnlyArray< + $ReadOnly<{ + errors: $ReadOnlyArray< + $ReadOnly<{ + data: mixed, + instancePath: string, + keyword: string, + message: string, + params: $ReadOnly<{ [string]: string }>, + parentSchema: RuleMetaData['schema'], + schema: mixed, + schemaPath: string, + }>>, + options: TOptions, + }>>, + valid: RuleTesterTests['valid'], + }>; diff --git a/yarn.lock b/yarn.lock index ee69267..d8cb2bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3019,6 +3019,27 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hermes-eslint@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.9.0.tgz#f1423abe3dfd959257430d61a9bccd4700b59e09" + integrity sha512-rlkK51UpGwo0ZWg8hu8DVICth7RfGSvaEJzFflos8bDOYm/d842/J3IXi0lB9R9waOp4VGGSc8VDmh+a9p2Q2w== + dependencies: + esrecurse "^4.3.0" + hermes-estree "0.9.0" + hermes-parser "0.9.0" + +hermes-estree@0.9.0, hermes-estree@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.9.0.tgz#026e0abe6db1dcf50a81a79014b779a83db3b814" + integrity sha512-5DZ7Y0CbHVk8zPqgRCvqp8iw+P05svnQDI1aJFjdqCfXJ/1CZ+8aYpGlhJ29zCG5SE5duGTzSxogAYYI4QqXqw== + +hermes-parser@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.9.0.tgz#ede3044d50479c61843cef5bbdcea83933d4e4ec" + integrity sha512-IcvJIlAn+9tpHkP+HTsxWKrIdQPp0gvGrrQmxlL4XnNS+Oh6R/Fpxbcoflm2kY3zgQjEvxZxLiK/2+k3/5wsrw== + dependencies: + hermes-estree "0.9.0" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"