Skip to content

Commit beec316

Browse files
committed
add duplicate id detection
also implements correctly "namespacing" for `<template>` elements
1 parent 3ee7cae commit beec316

27 files changed

+208
-34
lines changed

src/cli/lsp/logic.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ pub fn loadFile(
5757
.source = if (err.tag == .token) "html tokenizer" else "html parser",
5858
.relatedInformation = switch (err.tag) {
5959
else => null,
60+
.duplicate_id => |span| try arena.dupe(
61+
lsp.types.DiagnosticRelatedInformation,
62+
&.{
63+
.{
64+
.location = .{ .uri = uri, .range = getRange(
65+
span,
66+
doc.src,
67+
) },
68+
.message = "original",
69+
},
70+
},
71+
),
6072
.duplicate_class => |span| try arena.dupe(
6173
lsp.types.DiagnosticRelatedInformation,
6274
&.{

src/html/Ast.zig

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ pub const Error = struct {
280280
void_end_tag,
281281
duplicate_attribute_name: Span, // original attribute
282282
duplicate_sibling_attr: Span, // original attribute in another element
283+
duplicate_id: Span, // original location
283284
deprecated_and_unsupported,
284285

285286
const Tag = @This();
@@ -381,6 +382,10 @@ pub const Error = struct {
381382
"duplicate attribute name across sibling elements",
382383
.{},
383384
),
385+
.duplicate_id => w.print(
386+
"duplicate id value",
387+
.{},
388+
),
384389
.deprecated_and_unsupported => w.print("deprecated and unsupported", .{}),
385390
};
386391
}
@@ -417,8 +422,10 @@ fn printSourceLine(src: []const u8, span: Span, w: *Writer) !void {
417422
// test.html:3:7: invalid attribute for this element
418423
// <div foo bar baz>
419424
// ^^^
420-
//
421-
var idx = span.start;
425+
426+
// If the error starts on a newline (eg `foo="bar\n`), we want to consider
427+
// it ast part of the previous line.
428+
var idx = span.start -| 1;
422429
var spaces_left: u32 = 0;
423430
const line_start = while (idx > 0) : (idx -= 1) switch (src[idx]) {
424431
'\n' => break idx + 1,
@@ -427,7 +434,7 @@ fn printSourceLine(src: []const u8, span: Span, w: *Writer) !void {
427434
} else 0;
428435

429436
idx = span.start;
430-
var last_non_space = idx;
437+
var last_non_space = idx -| 1; // if span.start is a newline don't print it
431438
while (idx < src.len) : (idx += 1) switch (src[idx]) {
432439
'\n' => break,
433440
' ', '\t', ('\n' + 1)...'\r' => {},
@@ -465,6 +472,14 @@ pub fn init(
465472
var seen_attrs: std.StringHashMapUnmanaged(Span) = .empty;
466473
defer seen_attrs.deinit(gpa);
467474

475+
// It's a stack because of <template> (which can also be nested)
476+
var seen_ids_stack: std.ArrayList(std.StringHashMapUnmanaged(Span)) = .empty;
477+
try seen_ids_stack.append(gpa, .empty);
478+
defer {
479+
for (seen_ids_stack.items) |*seen_ids| seen_ids.deinit(gpa);
480+
seen_ids_stack.deinit(gpa);
481+
}
482+
468483
var has_syntax_errors = false;
469484

470485
try nodes.append(.{
@@ -591,13 +606,18 @@ pub fn init(
591606
language,
592607
&errors,
593608
&seen_attrs,
609+
&seen_ids_stack.items[seen_ids_stack.items.len - 1],
594610
nodes.items,
595611
parent_idx,
596612
src,
597613
tag.span,
598614
@intCast(nodes.items.len),
599615
);
600616

617+
if (kind == .template) {
618+
try seen_ids_stack.append(gpa, .empty);
619+
}
620+
601621
break :node .{
602622
.open = tag.span,
603623
.kind = kind,
@@ -797,7 +817,13 @@ pub fn init(
797817
if (std.ascii.eqlIgnoreCase(current_name, "math")) {
798818
math_lvl -= 1;
799819
}
820+
if (current.kind == .template) {
821+
var map = seen_ids_stack.pop().?;
822+
map.deinit(gpa);
823+
}
824+
800825
current.close = tag.span;
826+
801827
var cur = original_current;
802828
while (cur != current) {
803829
if (!cur.isClosed()) {
@@ -939,6 +965,8 @@ pub fn init(
939965
if (strict and !has_syntax_errors and language == .html) try validateNesting(
940966
gpa,
941967
nodes.items,
968+
&seen_attrs,
969+
&seen_ids_stack,
942970
&errors,
943971
src,
944972
language,
@@ -1374,6 +1402,8 @@ pub fn render(ast: Ast, src: []const u8, w: *Writer) !void {
13741402
pub fn validateNesting(
13751403
gpa: Allocator,
13761404
nodes: []const Node,
1405+
seen_attrs: *std.StringHashMapUnmanaged(Span),
1406+
seen_ids_stack: *std.ArrayList(std.StringHashMapUnmanaged(Span)),
13771407
errors: *std.ArrayListUnmanaged(Error),
13781408
src: []const u8,
13791409
language: Language,
@@ -1409,6 +1439,11 @@ pub fn validateNesting(
14091439
.text,
14101440
.doctype,
14111441
=> {
1442+
if (std.debug.runtime_safety and n.kind.isElement()) {
1443+
const element: Element = elements.get(n.kind);
1444+
assert(element.attributes != .manual);
1445+
}
1446+
14121447
var next = n;
14131448
node_idx = while (true) {
14141449
if (next.next_idx != 0) break next.next_idx;
@@ -1420,15 +1455,29 @@ pub fn validateNesting(
14201455
else => {},
14211456
}
14221457

1423-
defer node_idx += 1;
14241458
const element: Element = elements.get(n.kind);
14251459
try element.validateContent(
14261460
gpa,
14271461
nodes,
1462+
seen_attrs,
1463+
&seen_ids_stack.items[seen_ids_stack.items.len - 1],
14281464
errors,
14291465
src,
14301466
node_idx,
14311467
);
1468+
1469+
if (n.kind == .template) try seen_ids_stack.append(gpa, .empty);
1470+
1471+
var next = n;
1472+
node_idx = while (true) {
1473+
if (next.kind == .template) {
1474+
var map = seen_ids_stack.pop().?;
1475+
map.deinit(gpa);
1476+
}
1477+
if (next.next_idx != 0) break next.next_idx;
1478+
if (next.parent_idx == 0) return;
1479+
next = nodes[next.parent_idx];
1480+
};
14321481
}
14331482
}
14341483

@@ -2075,7 +2124,11 @@ test "fuzz" {
20752124

20762125
var in: Reader = .fixed(input);
20772126
var out: Writer.Allocating = .init(ctx.gpa);
2078-
try generator.generate(ctx.gpa, &in, &out.writer);
2127+
generator.generate(ctx.gpa, &in, &out.writer) catch |err| {
2128+
if (err == error.Skip) return;
2129+
return err;
2130+
};
2131+
20792132
std.debug.print("--begin--\n{s}\n\n", .{out.written()});
20802133

20812134
const ast: Ast = try .init(ctx.gpa, out.written(), .html, false);
@@ -2084,6 +2137,10 @@ test "fuzz" {
20842137
if (!ast.has_syntax_errors) {
20852138
try ast.render(out.written(), &devnull.writer);
20862139
}
2140+
2141+
if (ast.errors.len > 0) {
2142+
try ast.printErrors(out.written(), null, &devnull.writer);
2143+
}
20872144
}
20882145
};
20892146

src/html/Attribute.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ pub const ValidatingIterator = struct {
825825
it: Tokenizer,
826826
errors: *std.ArrayListUnmanaged(Ast.Error),
827827
seen_attrs: *std.StringHashMapUnmanaged(Span),
828+
seen_ids: *std.StringHashMapUnmanaged(Span),
828829
end: u32,
829830
node_idx: u32,
830831
name: Span = undefined,
@@ -833,6 +834,7 @@ pub const ValidatingIterator = struct {
833834
pub fn init(
834835
errors: *std.ArrayListUnmanaged(Ast.Error),
835836
seen_attrs: *std.StringHashMapUnmanaged(Span),
837+
seen_ids: *std.StringHashMapUnmanaged(Span),
836838
lang: Language,
837839
tag: Span,
838840
src: []const u8,
@@ -847,6 +849,7 @@ pub const ValidatingIterator = struct {
847849
},
848850
.errors = errors,
849851
.seen_attrs = seen_attrs,
852+
.seen_ids = seen_ids,
850853
.end = tag.end,
851854
.node_idx = node_idx,
852855
};
@@ -889,6 +892,18 @@ pub const ValidatingIterator = struct {
889892
});
890893
continue;
891894
} else {
895+
if (std.ascii.eqlIgnoreCase(attr_name, "id")) {
896+
if (attr.value) |v| {
897+
const idgop = try vait.seen_ids.getOrPut(gpa, v.span.slice(src));
898+
if (idgop.found_existing) {
899+
try vait.errors.append(gpa, .{
900+
.tag = .{ .duplicate_id = idgop.value_ptr.* },
901+
.main_location = v.span,
902+
.node_idx = vait.node_idx,
903+
});
904+
} else idgop.value_ptr.* = v.span;
905+
}
906+
}
892907
gop.value_ptr.* = attr.name;
893908
return attr;
894909
}

src/html/Element.zig

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ content: union(enum) {
6767
validate: *const fn (
6868
gpa: Allocator,
6969
nodes: []const Ast.Node,
70+
seen_attrs: *std.StringHashMapUnmanaged(Span),
71+
seen_ids: *std.StringHashMapUnmanaged(Span),
7072
errors: *std.ArrayListUnmanaged(Ast.Error),
7173
src: []const u8,
7274
parent_idx: u32,
@@ -322,13 +324,23 @@ pub inline fn validateContent(
322324
parent_element: *const Element,
323325
gpa: Allocator,
324326
nodes: []const Ast.Node,
327+
seen_attrs: *std.StringHashMapUnmanaged(Span),
328+
seen_ids: *std.StringHashMapUnmanaged(Span),
325329
errors: *std.ArrayListUnmanaged(Ast.Error),
326330
src: []const u8,
327331
parent_idx: u32,
328332
) !void {
329333
content: switch (parent_element.content) {
330334
.anything => {},
331-
.custom => |custom| try custom.validate(gpa, nodes, errors, src, parent_idx),
335+
.custom => |custom| try custom.validate(
336+
gpa,
337+
nodes,
338+
seen_attrs,
339+
seen_ids,
340+
errors,
341+
src,
342+
parent_idx,
343+
),
332344
.model => continue :content .{ .simple = .{} },
333345
.simple => |simple| {
334346
const parent = nodes[parent_idx];
@@ -380,7 +392,7 @@ pub inline fn validateContent(
380392
.span = parent_span,
381393
},
382394
},
383-
.main_location = child.startTagIterator(src, .html).name_span,
395+
.main_location = child.span(src),
384396
.node_idx = child_idx,
385397
});
386398
continue :outer;
@@ -445,7 +457,7 @@ pub inline fn validateContent(
445457
.span = parent_span,
446458
},
447459
},
448-
.main_location = node.startTagIterator(src, .html).name_span,
460+
.main_location = node.span(src),
449461
.node_idx = node_idx,
450462
});
451463
continue :outer;
@@ -462,7 +474,7 @@ pub inline fn validateContent(
462474
.reason = "presence of [tabindex]",
463475
},
464476
},
465-
.main_location = node.startTagIterator(src, .html).name_span,
477+
.main_location = node.span(src),
466478
.node_idx = node_idx,
467479
});
468480
continue :outer;
@@ -478,6 +490,7 @@ pub inline fn validateAttrs(
478490
lang: Language,
479491
errors: *std.ArrayListUnmanaged(Error),
480492
seen_attrs: *std.StringHashMapUnmanaged(Span),
493+
seen_ids: *std.StringHashMapUnmanaged(Span),
481494
nodes: []const Ast.Node,
482495
parent_idx: u32,
483496
src: []const u8,
@@ -487,6 +500,7 @@ pub inline fn validateAttrs(
487500
var vait: Attribute.ValidatingIterator = .init(
488501
errors,
489502
seen_attrs,
503+
seen_ids,
490504
lang,
491505
tag,
492506
src,

src/html/elements/audio_video.zig

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ pub fn validateAttrs(
232232
pub fn validateContent(
233233
gpa: Allocator,
234234
nodes: []const Ast.Node,
235+
seen_attrs: *std.StringHashMapUnmanaged(Span),
236+
seen_ids: *std.StringHashMapUnmanaged(Span),
235237
errors: *std.ArrayListUnmanaged(Ast.Error),
236238
src: []const u8,
237239
parent_idx: u32,
@@ -251,8 +253,6 @@ pub fn validateContent(
251253
break :blk .{ it.name_span, false };
252254
};
253255

254-
var seen_attrs: std.StringHashMapUnmanaged(Span) = .empty;
255-
defer seen_attrs.deinit(gpa);
256256
var state: enum { source, track, rest } = if (has_src) .track else .source;
257257
var first_default: ?Span = null;
258258
var child_idx = parent.first_child_idx;
@@ -272,7 +272,8 @@ pub fn validateContent(
272272
try validateSource(
273273
gpa,
274274
errors,
275-
&seen_attrs,
275+
seen_attrs,
276+
seen_ids,
276277
src,
277278
parent.kind,
278279
child.open,
@@ -306,7 +307,8 @@ pub fn validateContent(
306307
try validateTrack(
307308
gpa,
308309
errors,
309-
&seen_attrs,
310+
seen_attrs,
311+
seen_ids,
310312
src,
311313
parent.kind,
312314
child.open,
@@ -368,6 +370,7 @@ fn validateSource(
368370
gpa: Allocator,
369371
errors: *std.ArrayList(Ast.Error),
370372
seen_attrs: *std.StringHashMapUnmanaged(Span),
373+
seen_ids: *std.StringHashMapUnmanaged(Span),
371374
src: []const u8,
372375
parent_kind: Ast.Kind,
373376
node_span: Span,
@@ -379,6 +382,7 @@ fn validateSource(
379382
var vait: ValidatingIterator = .init(
380383
errors,
381384
seen_attrs,
385+
seen_ids,
382386
.html,
383387
node_span,
384388
src,
@@ -437,6 +441,7 @@ fn validateTrack(
437441
gpa: Allocator,
438442
errors: *std.ArrayList(Ast.Error),
439443
seen_attrs: *std.StringHashMapUnmanaged(Span),
444+
seen_ids: *std.StringHashMapUnmanaged(Span),
440445
src: []const u8,
441446
parent_kind: Ast.Kind,
442447
node_span: Span,
@@ -466,6 +471,7 @@ fn validateTrack(
466471
var vait: ValidatingIterator = .init(
467472
errors,
468473
seen_attrs,
474+
seen_ids,
469475
.html,
470476
node_span,
471477
src,

src/html/elements/button.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,15 @@ fn validateAttrs(
472472
pub fn validateContent(
473473
gpa: Allocator,
474474
nodes: []const Ast.Node,
475+
seen_attrs: *std.StringHashMapUnmanaged(Span),
476+
seen_ids: *std.StringHashMapUnmanaged(Span),
475477
errors: *std.ArrayListUnmanaged(Ast.Error),
476478
src: []const u8,
477479
parent_idx: u32,
478480
) error{OutOfMemory}!void {
481+
_ = seen_attrs;
482+
_ = seen_ids;
483+
479484
const parent = nodes[parent_idx];
480485
const parent_span = parent.startTagIterator(src, .html).name_span;
481486
const first_child_idx = parent.first_child_idx;

0 commit comments

Comments
 (0)