Skip to content

Bug: Reserved words not recognized as property identifiers after optional chaining (?.) #377

@rbonestell

Description

@rbonestell

The following pieces of code are valid but parsed incorrectly:

a?.delete(b);
myMap?.delete(key);
a?.class;
a?.return;

The output of tree-sitter parse is the following:

a.delete(b); parses correctly:

(program [0, 0] - [1, 0]
  (expression_statement [0, 0] - [0, 12]
    (call_expression [0, 0] - [0, 11]
      function: (member_expression [0, 0] - [0, 8]
        object: (identifier [0, 0] - [0, 1])
        property: (property_identifier [0, 2] - [0, 8]))
      arguments: (arguments [0, 8] - [0, 11]
        (identifier [0, 9] - [0, 10])))))

a?.delete(b); produces an ERROR node:

(program [0, 0] - [1, 0]
  (expression_statement [0, 0] - [0, 13]
    (call_expression [0, 0] - [0, 12]
      function: (identifier [0, 0] - [0, 1])
      (ERROR [0, 3] - [0, 9])
      arguments: (arguments [0, 9] - [0, 12]
        (identifier [0, 10] - [0, 11])))))

The delete keyword (and all other globally-reserved words) is correctly treated as a property_identifier after . but produces an ERROR node after ?. (optional chaining).

All globally-reserved words are affected:

a?.delete(b);  // ERROR
a?.class;      // ERROR
a?.return;     // ERROR

Analysis

The member_expression rule in grammar.js (lines 884-891) uses reserved('properties', ...) to allow reserved words as property identifiers:

member_expression: $ => prec('member', seq(
  field('object', choice($.expression, $.primary_expression, $.import)),
  choice('.', field('optional_chain', $.optional_chain)),
  field('property', choice(
    $.private_property_identifier,
    reserved('properties', alias($.identifier, $.property_identifier)),
  )),
)),

The properties reserved list is empty (line 72), meaning all globally-reserved words should be permitted in property position. This works correctly when the preceding token is ., but not when it is ?. (optional_chain, which is an external token defined at line 855).

The reserved feature (introduced in tree-sitter v0.25) may not be applying its keyword-to-identifier demotion when the context involves external tokens like optional_chain. This could also be a tree-sitter core issue rather than a grammar issue.

Real-World Impact

This affects real-world codebases significantly. Map.prototype.delete() and Set.prototype.delete() are commonly called via optional chaining (map?.delete(key)). Parsing the current microsoft/vscode repository with tree-sitter-typescript (which inherits this grammar) produces 40+ parse errors, nearly all from ?.delete() calls.

Environment

  • tree-sitter CLI: v0.26.6
  • tree-sitter-javascript: v0.25.0
  • Platform: macOS (darwin arm64), also reproduced on Linux and Windows via CI

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions