Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6393,6 +6393,7 @@ pub const Parser = struct {
}
// TS1214: strict reserved words cannot be used as import alias names in strict mode.
try self.checkStrictBinding(self.tokIdx());
const name_tok = self.tokIdx(); // the binding identifier `X` (before `=`)
_ = self.advance(); // eat name
_ = self.advance(); // eat '='
// TS1202 (`import X = require("mod")` requires module: commonjs) is a
Expand Down Expand Up @@ -6426,14 +6427,31 @@ pub const Parser = struct {
} else if (cur != .identifier and !cur.isKeyword() and cur != .kw_from and cur != .kw_of) {
try self.emitDiagnostic(self.currentSpan(), "Identifier expected", .{});
}
// Declare the local binding `X` so references resolve to it, mirroring
// the namespace (`import * as X`) and default-import paths. Emitted
// after the error guards above so a rejected `require(nonLiteral)`
// leaves no orphaned symbol (#64).
const local_node = try self.addNode(.{
.tag = .identifier,
.main_token = name_tok,
.data = .{ .lhs = .none, .rhs = .none },
});
try self.emitDeclare(.import_binding, local_node);
// `require('...')` or qualified name `A.B.C`
const module_ref = try self.parseAssignmentExpression();
_ = self.eat(.semicolon);
return self.addNode(.{
const decl = try self.addNode(.{
.tag = .import_decl,
.main_token = import_tok,
.data = .{ .lhs = .none, .rhs = module_ref },
});
// Anchor the binding under the declaration. `import_decl` uses lhs as
// the import-equals discriminator (.none) and rhs as the module
// reference, so the binding has no data slot — link it via
// parent_fixups (child, parent) for parent-tree walks.
try self.parent_fixups.append(self.gpa, @intFromEnum(local_node));
try self.parent_fixups.append(self.gpa, @intFromEnum(decl));
return decl;
}
// Not an import alias — reset position
self.tok_i = start_tok;
Expand Down
22 changes: 22 additions & 0 deletions tests/parser_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1021,3 +1021,25 @@ test "arrow's actual return_type node is the parenthesized type (#62)" {
// The annotation's inner node (data.lhs) is the parenthesized type.
try expectNodeTag(&tree, datas[ad.return_type.toInt()].lhs, .ts_parenthesized_type);
}

test "import-equals binding identifier is created and anchored to the declaration (#64)" {
const src = "import mod = require(\"./m\");";
var lr = try Lexer.tokenizeWithLanguage(testing.allocator, src, .ts);
defer lr.deinit(testing.allocator);
var tree = try Parser.parseWithOptions(testing.allocator, src, lr.tokens.slice(), .{ .language = .ts, .is_module = true });
defer tree.deinit(testing.allocator);
try expectNoErrors(&tree);
const decl = firstNodeOfTag(&tree, .import_decl) orelse return error.NoDecl;
const parents = try es_parser.parent_builder.buildParentsOnly(&tree, testing.allocator);
defer testing.allocator.free(parents);
// The binding identifier `mod` is created and parented directly to the
// import_decl (the `require(...)` call's own `require` identifier is parented
// to the call, so the only identifier child of the declaration is the binding).
const tags = tree.nodes.items(.tag);
var binding_children: u32 = 0;
var i: u32 = 0;
while (i < tree.nodes.len) : (i += 1) {
if (tags[i] == .identifier and parents[i] == @intFromEnum(decl)) binding_children += 1;
}
try testing.expectEqual(@as(u32, 1), binding_children);
}
26 changes: 26 additions & 0 deletions tests/semantic_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1539,3 +1539,29 @@ test "conditional with a paren-expression alternate leaks no phantom scope (#62)
}
try testing.expectEqual(@as(u32, 1), b_params);
}

test "import-equals declares an import_binding that references resolve to (#64)" {
// `import X = require(...)` and `import X = Y.Z` must declare the local `X`,
// like `import * as X` and default imports — previously no symbol was created.
{
var r = try analyzeTsModuleSource("import mod = require(\"./m\");\nmod;");
defer r.deinit(testing.allocator);
const sym = findSymbolByKind(&r, "mod", .import_binding) orelse return error.ImportNotFound;
try testing.expect(r.symbols.getRefRange(sym).len() >= 1); // the `mod;` usage
}
{
var r = try analyzeTsModuleSource("import X = Y.Z;\nX;");
defer r.deinit(testing.allocator);
const sym = findSymbolByKind(&r, "X", .import_binding) orelse return error.ImportNotFound;
try testing.expect(r.symbols.getRefRange(sym).len() >= 1);
}
}

test "import-equals binding does not collide with a same-named reference (#64)" {
// `import C = N.C; class D extends C {}` — the `extends C` resolves to the
// import binding (the collision case from the issue), not a name-only fallback.
var r = try analyzeTsModuleSource("import C = N.C;\nclass D extends C {}");
defer r.deinit(testing.allocator);
const imp = findSymbolByKind(&r, "C", .import_binding) orelse return error.ImportNotFound;
try testing.expect(r.symbols.getRefRange(imp).len() >= 1); // `extends C`
}
Loading