facebook/flow · error · Error

Unexpected selector

Error message

Unexpected selector ${parsedSelector.value}

What it means

Plain Error thrown by flow-transform's traverse when a visitor key used as an esquery identifier selector is not a known Flow AST node type (validated against `FlowVisitorKeys` from flow-parser). Bare identifiers in selectors are interpreted as node type names, so a typo or a Babel/TypeScript/ESLint-only type name fails immediately when the NodeEventGenerator registers listeners.

Solutions

  1. Use Flow ESTree node type names - dump the AST with hermes-parser/flow-parser to confirm exact type strings.
  2. Map common renames: StringLiteral/NumberLiteral -> Literal, ClassProperty -> PropertyDefinition, TSTypeAnnotation -> Flow type annotation nodes.
  3. For non-type selectors use esquery syntax that does not require an identifier (attribute selectors, `*`).
  4. Check the name against FlowVisitorKeys (exported from flow-parser) at startup.

Example fix

// before
traverseWithContext(code, ast, scope, () => ({}), ctx => ({
  ClassProperty(node) {}, // not a Flow ESTree type - throws
}));

// after
traverseWithContext(code, ast, scope, () => ({}), ctx => ({
  PropertyDefinition(node) {},
}));
Defensive patterns

Strategy: validation

Validate before calling

import {FlowVisitorKeys} from 'flow-parser';

function validateVisitorKeys(selectors) {
  for (const sel of Object.keys(selectors)) {
    const base = sel.replace(/:exit$/u, '');
    // bare identifiers (no combinators/attributes) must be Flow node types
    if (/^[A-Za-z]+$/.test(base) && !(base in FlowVisitorKeys)) {
      throw new Error(`visitor key '${sel}' is not a Flow AST node type`);
    }
  }
}

validateVisitorKeys(selectors); // before traverseWithContext(...)

Type guard

function isFlowNodeType(name: string): boolean {
  const {FlowVisitorKeys} = require('flow-parser');
  return name in FlowVisitorKeys;
}

Prevention

When it happens

Trigger: `traverse(ast, {CallExpresion(node) {}})` (typo) or using Babel/TS names like `TSTypeAnnotation`, `ClassProperty`, `StringLiteral` - Flow's ESTree AST uses different names (e.g. `Literal`, `PropertyDefinition`). Thrown when traverse() builds its event generator, before any traversal.

Common situations: Porting ESLint rules or Babel plugin visitors to flow-transform; copying selector examples written against estree/babel node type lists; version drift where a node type was renamed in flow-estree.

Related errors


AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20). Data as JSON: /api/errors/51c2aa16635bdfa8. Report an issue: GitHub.

Appendix: source

Thrown at packages/flow-transform/src/traverse/NodeEventGenerator.js:78

  let result = [...new Set(arrays[0])];

  for (const array of arrays.slice(1)) {
    result = result.filter(x => array.includes(x));
  }
  return result;
}

/**
 * Gets the possible types of a selector
 * @param parsedSelector An object (from esquery) describing the matching behavior of the selector
 * @returns The node types that could possibly trigger this selector, or `null` if all node types could trigger it
 */
function getPossibleTypes(parsedSelector: Selector): ?Array<ESNode['type']> {
  switch (parsedSelector.type) {
    case 'identifier':
      if (!(parsedSelector.value in FlowVisitorKeys)) {
        throw new Error(`Unexpected selector ${parsedSelector.value}`);
      }
      // $FlowExpectedError[incompatible-type]
      return [parsedSelector.value];

    case 'matches': {
      const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
      const typesForComponentsNonNull = typesForComponents.filter(Boolean);

      if (typesForComponents.length === typesForComponentsNonNull.length) {
        return union(...typesForComponentsNonNull);
      }
      return null;
    }

    case 'compound': {
      const typesForComponents = parsedSelector.selectors
        .map(getPossibleTypes)
        .filter(Boolean);

View on GitHub (pinned to f88ac94bcf)