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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions