From eeaa997d1f6f31ccb90145dc4fa792b7e4ab6ea8 Mon Sep 17 00:00:00 2001 From: Boidushya Date: Tue, 1 Sep 2026 15:20:21 +0530 Subject: [PATCH] fix: skip comments when scanning block boundaries --- .changeset/block-comment-boundaries.md | 5 ++ packages/core/src/compiler.ts | 35 ++++++++++ packages/core/tests/compiler.test.ts | 93 ++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 .changeset/block-comment-boundaries.md diff --git a/.changeset/block-comment-boundaries.md b/.changeset/block-comment-boundaries.md new file mode 100644 index 0000000..43de037 --- /dev/null +++ b/.changeset/block-comment-boundaries.md @@ -0,0 +1,5 @@ +--- +"rics": patch +--- + +Skip `/* */` and `//` comments when scanning for the end of an `@if`, `@else`, `@mixin`, `@for`, or `@each` block. Previously `readBlock` tracked braces and quotes through comment text, so a lone apostrophe (`it's`, `user's`) or a stray brace inside a comment desynced the brace count, producing a false `UNCLOSED_BLOCK` error or silently swallowing rules. The same block-comment blind spot in expression-value scanning (`readUntil`) is fixed too. Comment text inside blocks is preserved unchanged. diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts index e53d499..5c139d1 100644 --- a/packages/core/src/compiler.ts +++ b/packages/core/src/compiler.ts @@ -245,6 +245,23 @@ class Compiler { return ""; } + private readRawComment(): string { + const start = this.pos; + if (this.peek() === "/" && this.peek(1) === "*") { + this.advance(2); + while (this.pos < this.input.length && !(this.peek() === "*" && this.peek(1) === "/")) { + this.advance(); + } + if (this.pos < this.input.length) this.advance(2); + } else { + this.advance(2); + while (this.pos < this.input.length && this.peek() !== "\n") { + this.advance(); + } + } + return this.input.slice(start, this.pos); + } + private parseStylesheet(): void { while (this.pos < this.input.length) { this.checkLimits(); @@ -1862,6 +1879,19 @@ class Compiler { continue; } + if (ch === "/" && this.peek(1) === "*") { + content += this.readRawComment(); + continue; + } + + if (ch === "/" && this.peek(1) === "/") { + const last = content.length > 0 ? content[content.length - 1] : ""; + if (last === "" || last === " " || last === "\t" || last === "\n" || last === "\r") { + content += this.readRawComment(); + continue; + } + } + if (ch === '"' || ch === "'") { inString = ch; content += this.advance(); @@ -1928,6 +1958,11 @@ class Compiler { continue; } + if (ch === "/" && this.peek(1) === "*") { + result += this.readRawComment(); + continue; + } + if (ch === "/" && this.peek(1) === "/") { const last = result.length > 0 ? result[result.length - 1] : ""; if (last === "" || last === " " || last === "\t" || last === "\n" || last === "\r") { diff --git a/packages/core/tests/compiler.test.ts b/packages/core/tests/compiler.test.ts index b81994a..45eabe8 100644 --- a/packages/core/tests/compiler.test.ts +++ b/packages/core/tests/compiler.test.ts @@ -553,6 +553,99 @@ describe("compiler", () => { }); }); + describe("block-boundary comments (issue #1)", () => { + it("should not break @if block on comment with an apostrophe", () => { + const { css, errors } = compileWithDetails(` + $mode: on; + @if $mode == on { + /* the cat's toy fell */ + .a { color: red; } + .b { color: blue; } + } + `); + expect(errors).toEqual([]); + expect(css).toContain(".a"); + expect(css).toContain(".b"); + expect(css).toContain("/* the cat's toy fell */"); + }); + + it("should not break @mixin block on comment with an apostrophe", () => { + const { css, errors } = compileWithDetails(` + @mixin extras() { + /* the cat's toy fell */ + .a { color: red; } + .b { color: blue; } + } + @include extras(); + `); + expect(errors).toEqual([]); + expect(css).toContain(".a"); + expect(css).toContain(".b"); + }); + + it("should not swallow rules when a block comment contains a brace", () => { + const { css, errors } = compileWithDetails(` + @mixin m() { + /* use } carefully */ + .a { color: red; } + } + @include m(); + `); + expect(errors).toEqual([]); + expect(css).toContain(".a"); + expect(css).toContain("color: red"); + expect(css).toContain("/* use } carefully */"); + }); + + it("should not break @for block on comment with an apostrophe and brace", () => { + const { css, errors } = compileWithDetails(` + @for $i from 1 through 2 { + /* it's item #{$i} { row } */ + .col-#{$i} { width: $i; } + } + `); + expect(errors).toEqual([]); + expect(css).toContain(".col-1"); + expect(css).toContain(".col-2"); + }); + + it("should not break @each block on comment with an apostrophe", () => { + const { css, errors } = compileWithDetails(` + @each $c in red, blue { + /* author's note */ + .#{$c} { color: $c; } + } + `); + expect(errors).toEqual([]); + expect(css).toContain(".red"); + expect(css).toContain(".blue"); + }); + + it("should not break @mixin block on a // line comment with apostrophe and brace", () => { + const { css, errors } = compileWithDetails(` + @mixin m() { + // it's a { comment + .a { color: red; } + } + @include m(); + `); + expect(errors).toEqual([]); + expect(css).toContain(".a"); + expect(css).toContain("color: red"); + expect(css).not.toContain("comment"); + }); + + it("should not desync a value on a block comment containing a brace or apostrophe", () => { + const { css, errors } = compileWithDetails(` + .a { margin: 1px /* it's } here */ 2px; } + .b { color: green; } + `); + expect(errors).toEqual([]); + expect(css).toContain(".b"); + expect(css).toContain("color: green"); + }); + }); + describe("error handling", () => { it("should report undefined variables", () => { const input = `.box { color: $undefined; }`;