From f675cb53f5b536d1cd7ae06dccc716e05a311344 Mon Sep 17 00:00:00 2001 From: Tomas Valent Date: Tue, 30 Sep 2025 06:40:19 +0200 Subject: [PATCH 01/97] Add Rubymine to list of Editors Herb Dev Tools (#538) Add Rubymine to the list inspired by https://github.com/igorkasyanchuk/editor_opener/blob/31b09cf4f62c1b84b10e111c91ce3ce3ff450a60/lib/editor_opener/action_dispatch/trace_to_file_extractor.rb#L13 & https://github.com/marcoroth/herb/pull/486/files Signed-off-by: Tomas Valent --- javascript/packages/dev-tools/src/herb-overlay.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/packages/dev-tools/src/herb-overlay.ts b/javascript/packages/dev-tools/src/herb-overlay.ts index 49cc8aff3..e2b12d4a1 100644 --- a/javascript/packages/dev-tools/src/herb-overlay.ts +++ b/javascript/packages/dev-tools/src/herb-overlay.ts @@ -934,6 +934,7 @@ export class HerbOverlay { `subl://open?url=file://${absolutePath}&line=${line}&column=${column}`, `atom://core/open/file?filename=${absolutePath}&line=${line}&column=${column}`, `txmt://open?url=file://${absolutePath}&line=${line}&column=${column}`, + `x-mine://open?file=//${absolutePath}&line=${line}&column=${column}`, ]; try { From 7249cd76c1ebaec6ae72a87b7693f91e84d39392 Mon Sep 17 00:00:00 2001 From: Domingo Edwards Date: Tue, 30 Sep 2025 02:14:29 -0300 Subject: [PATCH 02/97] Linter: Implement `erb-comment-syntax` linter rule (#528) Closes: #527 Advances: https://github.com/marcoroth/herb/issues/537 This PR adds the rule `erb-comment-syntax`. It is the same rule implemented in [CommentSyntax](https://github.com/Shopify/erb_lint?tab=readme-ov-file#commentsyntax) of ERB Lint. The rule itself avoid parsing errors in the action_view erb default parsing implementation. Also the porting of ERB Lint rules to herb rules facilitates the adoption of Herb as a ERB Lint replacement. --------- Co-authored-by: Marco Roth --- .../packages/linter/docs/rules/README.md | 1 + .../linter/docs/rules/erb-comment-syntax.md | 40 ++++++++++ .../packages/linter/src/default-rules.ts | 2 + .../linter/src/rules/erb-comment-syntax.ts | 30 ++++++++ javascript/packages/linter/src/rules/index.ts | 1 + .../test/__snapshots__/cli.test.ts.snap | 6 +- .../test/rules/erb-comment-syntax.test.ts | 75 +++++++++++++++++++ javascript/packages/vscode/package.json | 1 + 8 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 javascript/packages/linter/docs/rules/erb-comment-syntax.md create mode 100644 javascript/packages/linter/src/rules/erb-comment-syntax.ts create mode 100644 javascript/packages/linter/test/rules/erb-comment-syntax.test.ts diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md index 689d05bc4..4a361e7ec 100644 --- a/javascript/packages/linter/docs/rules/README.md +++ b/javascript/packages/linter/docs/rules/README.md @@ -4,6 +4,7 @@ This page contains documentation for all Herb Linter rules. ## Available Rules +- [`erb-comment-syntax`](./erb-comment-syntax.md) - Disallow Ruby comments immediately after ERB tags - [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags - [`erb-no-output-control-flow`](./erb-no-output-control-flow.md) - Prevents outputting control flow blocks - [`erb-no-silent-tag-in-attribute-name`](./erb-no-silent-tag-in-attribute-name.md) - Disallow ERB silent tags in HTML attribute names diff --git a/javascript/packages/linter/docs/rules/erb-comment-syntax.md b/javascript/packages/linter/docs/rules/erb-comment-syntax.md new file mode 100644 index 000000000..a4891b51f --- /dev/null +++ b/javascript/packages/linter/docs/rules/erb-comment-syntax.md @@ -0,0 +1,40 @@ +# Linter Rule: Disallow Ruby comments immediately after ERB tags + +**Rule:** `erb-comment-syntax` + +## Description + +Disallow ERB tags that start with `<% #` (with a space before the `#`). Use the ERB comment syntax `<%#` instead. + +## Rationale + +Ruby comments starting immediately after an ERB tag opening (e.g., `<% # comment %>`) can cause parsing issues in some contexts. The proper ERB comment syntax `<%# comment %>` is more reliable and explicitly designed for comments in templates. + +For multi-line comments or actual Ruby code with comments, ensure the content starts on a new line after the opening tag. + +## Examples + +### ✅ Good + +```erb +<%# This is a proper ERB comment %> + +<% + # This is a proper ERB comment +%> + +<% + # Multi-line Ruby comment + # spanning multiple lines +%> +``` + +### 🚫 Bad + +```erb +<% # This should be an ERB comment %> + +<%= # This should also be an ERB comment %> + +<%== # This should also be an ERB comment %> +``` diff --git a/javascript/packages/linter/src/default-rules.ts b/javascript/packages/linter/src/default-rules.ts index 5e6a04926..8dcd84e9f 100644 --- a/javascript/packages/linter/src/default-rules.ts +++ b/javascript/packages/linter/src/default-rules.ts @@ -1,5 +1,6 @@ import type { RuleClass } from "./types.js" +import { ERBCommentSyntax } from "./rules/erb-comment-syntax.js"; import { ERBNoEmptyTagsRule } from "./rules/erb-no-empty-tags.js" import { ERBNoOutputControlFlowRule } from "./rules/erb-no-output-control-flow.js" import { ERBNoSilentTagInAttributeNameRule } from "./rules/erb-no-silent-tag-in-attribute-name.js" @@ -36,6 +37,7 @@ import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalizatio import { HTMLNoUnderscoresInAttributeNamesRule } from "./rules/html-no-underscores-in-attribute-names.js" export const defaultRules: RuleClass[] = [ + ERBCommentSyntax, ERBNoEmptyTagsRule, ERBNoOutputControlFlowRule, ERBNoSilentTagInAttributeNameRule, diff --git a/javascript/packages/linter/src/rules/erb-comment-syntax.ts b/javascript/packages/linter/src/rules/erb-comment-syntax.ts new file mode 100644 index 000000000..aabed1d1f --- /dev/null +++ b/javascript/packages/linter/src/rules/erb-comment-syntax.ts @@ -0,0 +1,30 @@ +import { BaseRuleVisitor } from "./rule-utils.js" +import { ParserRule } from "../types.js" + +import type { LintOffense, LintContext } from "../types.js" +import type { ParseResult, ERBContentNode } from "@herb-tools/core" + +class ERBCommentSyntaxVisitor extends BaseRuleVisitor { + visitERBContentNode(node: ERBContentNode): void { + if (node.content?.value.startsWith(" #")) { + const openingTag = node.tag_opening?.value + + this.addOffense( + `Use \`<%#\` instead of \`${openingTag} #\`. Ruby comments immediately after ERB tags can cause parsing issues.`, + node.location + ) + } + } +} + +export class ERBCommentSyntax extends ParserRule { + name = "erb-comment-syntax" + + check(result: ParseResult, context?: Partial): LintOffense[] { + const visitor = new ERBCommentSyntaxVisitor(this.name, context) + + visitor.visit(result.value) + + return visitor.offenses + } +} diff --git a/javascript/packages/linter/src/rules/index.ts b/javascript/packages/linter/src/rules/index.ts index 29a4ce56a..ce38e34f0 100644 --- a/javascript/packages/linter/src/rules/index.ts +++ b/javascript/packages/linter/src/rules/index.ts @@ -1,4 +1,5 @@ export * from "./rule-utils.js" +export * from "./erb-comment-syntax.js" export * from "./erb-no-empty-tags.js" export * from "./erb-no-output-control-flow.js" export * from "./erb-no-silent-tag-in-attribute-name.js" diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index 836e0922b..bf976497d 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -561,7 +561,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for bad file 1`] "summary": { "filesChecked": 1, "filesWithOffenses": 1, - "ruleCount": 31, + "ruleCount": 32, "totalErrors": 2, "totalOffenses": 2, "totalWarnings": 0, @@ -579,7 +579,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for clean file 1` "summary": { "filesChecked": 1, "filesWithOffenses": 0, - "ruleCount": 31, + "ruleCount": 32, "totalErrors": 0, "totalOffenses": 0, "totalWarnings": 0, @@ -649,7 +649,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for file with err "summary": { "filesChecked": 1, "filesWithOffenses": 1, - "ruleCount": 31, + "ruleCount": 32, "totalErrors": 3, "totalOffenses": 3, "totalWarnings": 0, diff --git a/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts new file mode 100644 index 000000000..8bc529690 --- /dev/null +++ b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts @@ -0,0 +1,75 @@ +import dedent from "dedent" +import { describe, test, expect, beforeAll } from "vitest" +import { Herb } from "@herb-tools/node-wasm" +import { Linter } from "../../src/linter.js" +import { ERBCommentSyntax } from "../../src/rules/erb-comment-syntax.js" + +describe("ERBCommentSyntax", () => { + beforeAll(async () => { + await Herb.load() + }) + + test("when the ERB comment syntax is correct", () => { + const html = dedent` + <%# good comment %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(0) + }) + + test("when the ERB multi-line comment syntax is correct", () => { + const html = dedent` + <% + # good comment + %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(0) + }) + + test("when the ERB multi-line comment syntax is correct with multiple comment lines", () => { + const html = dedent` + <% + # good comment + # good comment + %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(0) + }) + + test("when the ERB comment syntax is incorrect", () => { + const html = dedent` + <% # bad comment %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(1) + expect(lintResult.offenses[0].message).toBe("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") + }) + + test("when the ERB comment syntax is incorrect multiple times in one file", () => { + const html = dedent` + <% # first bad comment %> + <%= # second bad comment %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(2) + expect(lintResult.offenses[0].message).toBe("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") + expect(lintResult.offenses[1].message).toBe("Use `<%#` instead of `<%= #`. Ruby comments immediately after ERB tags can cause parsing issues.") + }) +}) diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json index 8dd528949..571a05a17 100644 --- a/javascript/packages/vscode/package.json +++ b/javascript/packages/vscode/package.json @@ -61,6 +61,7 @@ "items": { "type": "string", "enum": [ + "erb-comment-syntax", "erb-no-empty-tags", "erb-no-output-control-flow", "erb-no-silent-tag-in-attribute-name", From 5da59fc7c24ccb6986e5fe88a3be292d22e5ec7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= <3810945+timkaechele@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:44:34 +0200 Subject: [PATCH 03/97] C: Implement more efficient buffer resizing (#539) ## Problem If the buffer is nearly full even one extra character can trigger an expansion of the buffer. Since the buffer only grows by twice the number of required characters per resize, in this case 2 characters, the buffer has to be constantly resized. ### Visualization ![buffer_problem](https://github.com/user-attachments/assets/11658cf7-1ab5-41fe-8a75-fc6eaddfb80a) ## How the problem is addressed Rather than just checking the required length, we test whether doubling the current capacity will be enough. If it is, we expand to the doubled capacity. If not, we double the required length itself and resize the buffer to that size. ## Performance impact Lexing a [real world html page](https://shop.herthabsc.com) Before: ~88.255ms After: ~79.208ms Parsing showed no significant performance impact though --- src/buffer.c | 11 ++++++++++- test/c/test_buffer.c | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/buffer.c b/src/buffer.c index 750e2190d..f162c4116 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -114,7 +114,16 @@ bool buffer_expand_capacity(buffer_T* buffer) { bool buffer_expand_if_needed(buffer_T* buffer, const size_t required_length) { if (buffer_has_capacity(buffer, required_length)) { return true; } - return buffer_resize(buffer, buffer->capacity + (required_length * 2)); + bool should_double_capacity = required_length < buffer->capacity; + size_t new_capacity = 0; + + if (should_double_capacity) { + new_capacity = buffer->capacity * 2; + } else { + new_capacity = buffer->capacity + (required_length * 2); + } + + return buffer_resize(buffer, new_capacity); } /** diff --git a/test/c/test_buffer.c b/test/c/test_buffer.c index 27d7b8609..054b2d698 100644 --- a/test/c/test_buffer.c +++ b/test/c/test_buffer.c @@ -113,6 +113,20 @@ TEST(test_buffer_expand_if_needed) buffer_free(&buffer); END +TEST(test_buffer_expand_if_needed_with_nearly_full_buffer) + buffer_T buffer = buffer_new(); + + ck_assert_int_eq(buffer.capacity, 1024); + + buffer_append_repeated(&buffer, ' ', 1023); + ck_assert_int_eq(buffer.capacity, 1024); + + ck_assert(buffer_expand_if_needed(&buffer, 2)); + ck_assert_int_eq(buffer.capacity, 2048); + + buffer_free(&buffer); +END + // Test resizing buffer TEST(test_buffer_resize) buffer_T buffer = buffer_new(); @@ -229,6 +243,7 @@ TCase *buffer_tests(void) { tcase_add_test(buffer, test_buffer_increase_capacity); tcase_add_test(buffer, test_buffer_expand_capacity); tcase_add_test(buffer, test_buffer_expand_if_needed); + tcase_add_test(buffer, test_buffer_expand_if_needed_with_nearly_full_buffer); tcase_add_test(buffer, test_buffer_resize); tcase_add_test(buffer, test_buffer_clear); tcase_add_test(buffer, test_buffer_free); From 35124fc2ac558a816279f046b6af36e9bef18e69 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 1 Oct 2025 18:19:52 +0900 Subject: [PATCH 04/97] Parser: Fix parsing boolean attributes with `track_whitespace` (#560) --- .../test/rules/html-no-empty-headings.test.ts | 59 +++++++++------ .../test/rules/parser-no-errors.test.ts | 13 ++++ src/lexer_peek_helpers.c | 2 + test/parser/boolean_attributes_test.rb | 16 ++++ ...29e8b-b26dbda6d8a652930695c93bd07179f4.txt | 40 ++++++++++ ...901a9-b26dbda6d8a652930695c93bd07179f4.txt | 37 +++++++++ ...5b3ec-b26dbda6d8a652930695c93bd07179f4.txt | 75 +++++++++++++++++++ 7 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt create mode 100644 test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt create mode 100644 test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt diff --git a/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts b/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts index a27e3e4ed..151398aec 100644 --- a/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts +++ b/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts @@ -10,7 +10,7 @@ describe("html-no-empty-headings", () => { test("passes for heading with text content", () => { const html = '

Heading Content

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -21,7 +21,7 @@ describe("html-no-empty-headings", () => { test("passes for heading with nested elements", () => { const html = '

Text

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -32,7 +32,7 @@ describe("html-no-empty-headings", () => { test("passes for heading with ERB content", () => { const html = '

<%= title %>

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -43,7 +43,7 @@ describe("html-no-empty-headings", () => { test("fails for empty heading", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -58,7 +58,7 @@ describe("html-no-empty-headings", () => { test("fails for heading with only whitespace", () => { const html = '

\n\t

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -72,7 +72,7 @@ describe("html-no-empty-headings", () => { test("fails for self-closing heading", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -86,7 +86,7 @@ describe("html-no-empty-headings", () => { test("handles all heading levels h1-h6", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -101,7 +101,7 @@ describe("html-no-empty-headings", () => { test("handles mixed case heading tags", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -111,7 +111,7 @@ describe("html-no-empty-headings", () => { test("ignores non-heading tags", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -121,7 +121,7 @@ describe("html-no-empty-headings", () => { test("passes for headings with mixed content", () => { const html = '

Welcome <%= user.name %>!

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -131,7 +131,7 @@ describe("html-no-empty-headings", () => { test("passes for heading with only ERB", () => { const html = '

<%= page.title %>

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -141,7 +141,7 @@ describe("html-no-empty-headings", () => { test("handles multiple empty headings", () => { const html = '

Valid

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -152,7 +152,7 @@ describe("html-no-empty-headings", () => { test("passes for div with role='heading' and content", () => { const html = '
Heading Content
' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -163,7 +163,7 @@ describe("html-no-empty-headings", () => { test("fails for empty div with role='heading'", () => { const html = '
' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -178,7 +178,7 @@ describe("html-no-empty-headings", () => { test("fails for div with role='heading' containing only whitespace", () => { const html = '
' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -191,7 +191,7 @@ describe("html-no-empty-headings", () => { test("fails for self-closing div with role='heading'", () => { const html = '
' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -204,7 +204,7 @@ describe("html-no-empty-headings", () => { test("ignores div without role='heading'", () => { const html = '
Button
' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -215,7 +215,7 @@ describe("html-no-empty-headings", () => { test("handles mixed standard headings and ARIA headings", () => { const html = '

Valid

Valid

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -229,7 +229,7 @@ describe("html-no-empty-headings", () => { test("fails for heading with only aria-hidden content", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -244,7 +244,7 @@ describe("html-no-empty-headings", () => { test("fails for heading with mixed accessible and inaccessible content", () => { const html = '

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -255,7 +255,7 @@ describe("html-no-empty-headings", () => { test("passes for heading with mix of accessible and inaccessible content", () => { const html = '

Visible text

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -266,7 +266,7 @@ describe("html-no-empty-headings", () => { test("passes for heading itself with aria-hidden='true' but has content", () => { const html = '

Heading Content

' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -277,7 +277,7 @@ describe("html-no-empty-headings", () => { test("passes for heading itself with hidden attribute but has content", () => { const html = '' - + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) @@ -288,7 +288,18 @@ describe("html-no-empty-headings", () => { test("passes for heading with nested span containing text", () => { const html = '

Text

' - + + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(0) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(0) + }) + + test("passes for heading with nested span containing text", () => { + const html = '

<%= content %>

' + const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule]) const lintResult = linter.lint(html) diff --git a/javascript/packages/linter/test/rules/parser-no-errors.test.ts b/javascript/packages/linter/test/rules/parser-no-errors.test.ts index ec1c4277b..2fde56509 100644 --- a/javascript/packages/linter/test/rules/parser-no-errors.test.ts +++ b/javascript/packages/linter/test/rules/parser-no-errors.test.ts @@ -205,4 +205,17 @@ describe("ParserNoErrorsRule", () => { expect(lintResult.offenses[2].message).toBe("Opening tag `

` at (1:1) doesn't have a matching closing tag `

`. (`MISSING_CLOSING_TAG_ERROR`)") expect(lintResult.offenses[3].message).toBe("Opening tag `

` at (1:25) doesn't have a matching closing tag `

`. (`MISSING_CLOSING_TAG_ERROR`)") }) + + test("html element ending with boolean attribute followed by ERB tag", () => { + const html = dedent` + + <%= hello %> + ` + + const linter = new Linter(Herb, [ParserNoErrorsRule ]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(0) + expect(lintResult.offenses).toHaveLength(0) + }) }) diff --git a/src/lexer_peek_helpers.c b/src/lexer_peek_helpers.c index ca045ba31..20bccfc19 100644 --- a/src/lexer_peek_helpers.c +++ b/src/lexer_peek_helpers.c @@ -77,6 +77,7 @@ bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T tok size_t saved_line = lexer->current_line; size_t saved_column = lexer->current_column; char saved_character = lexer->current_character; + lexer_state_T saved_state = lexer->state; token_T* token = lexer_next_token(lexer); @@ -93,6 +94,7 @@ bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T tok lexer->current_line = saved_line; lexer->current_column = saved_column; lexer->current_character = saved_character; + lexer->state = saved_state; return result; } diff --git a/test/parser/boolean_attributes_test.rb b/test/parser/boolean_attributes_test.rb index 076935b8f..24d96191c 100644 --- a/test/parser/boolean_attributes_test.rb +++ b/test/parser/boolean_attributes_test.rb @@ -29,5 +29,21 @@ class BooleanAttributesTest < Minitest::Spec test "boolean attribute surrounded by regular attributes" do assert_parsed_snapshot(%()) end + + test "boolean attribute on void element followed by newline and ERB tag with track_whitespace" do + assert_parsed_snapshot(%(\n<%= hello %>), track_whitespace: true) + end + + test "boolean attribute on void element followed by ERB tag with track_whitespace" do + assert_parsed_snapshot(%(<%= hello %>), track_whitespace: true) + end + + test "boolean attribute on void element followed by ERB tag with track_whitespace" do + assert_parsed_snapshot(<<~HTML, track_whitespace: true) + + HTML + end end end diff --git a/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt new file mode 100644 index 000000000..6574e7dac --- /dev/null +++ b/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt @@ -0,0 +1,40 @@ +@ DocumentNode (location: (1:0)-(2:12)) +└── children: (3 items) + ├── @ HTMLElementNode (location: (1:0)-(1:18)) + │ ├── open_tag: + │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:18)) + │ │ ├── tag_opening: "<" (location: (1:0)-(1:1)) + │ │ ├── tag_name: "link" (location: (1:1)-(1:5)) + │ │ ├── tag_closing: ">" (location: (1:17)-(1:18)) + │ │ ├── children: (2 items) + │ │ │ ├── @ WhitespaceNode (location: (1:5)-(1:6)) + │ │ │ │ └── value: " " (location: (1:5)-(1:6)) + │ │ │ │ + │ │ │ └── @ HTMLAttributeNode (location: (1:6)-(1:17)) + │ │ │ ├── name: + │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:6)-(1:17)) + │ │ │ │ └── children: (1 item) + │ │ │ │ └── @ LiteralNode (location: (1:6)-(1:17)) + │ │ │ │ └── content: "crossorigin" + │ │ │ │ + │ │ │ │ + │ │ │ ├── equals: ∅ + │ │ │ └── value: ∅ + │ │ │ + │ │ └── is_void: false + │ │ + │ ├── tag_name: "link" (location: (1:1)-(1:5)) + │ ├── body: [] + │ ├── close_tag: ∅ + │ ├── is_void: true + │ └── source: "HTML" + │ + ├── @ HTMLTextNode (location: (2:3)-(2:0)) + │ └── content: "\n" + │ + └── @ ERBContentNode (location: (2:0)-(2:12)) + ├── tag_opening: "<%=" (location: (2:0)-(2:3)) + ├── content: " hello " (location: (2:3)-(2:10)) + ├── tag_closing: "%>" (location: (2:10)-(2:12)) + ├── parsed: true + └── valid: true \ No newline at end of file diff --git a/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt new file mode 100644 index 000000000..535ea29fc --- /dev/null +++ b/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt @@ -0,0 +1,37 @@ +@ DocumentNode (location: (1:0)-(1:30)) +└── children: (2 items) + ├── @ HTMLElementNode (location: (1:0)-(1:18)) + │ ├── open_tag: + │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:18)) + │ │ ├── tag_opening: "<" (location: (1:0)-(1:1)) + │ │ ├── tag_name: "link" (location: (1:1)-(1:5)) + │ │ ├── tag_closing: ">" (location: (1:17)-(1:18)) + │ │ ├── children: (2 items) + │ │ │ ├── @ WhitespaceNode (location: (1:5)-(1:6)) + │ │ │ │ └── value: " " (location: (1:5)-(1:6)) + │ │ │ │ + │ │ │ └── @ HTMLAttributeNode (location: (1:6)-(1:17)) + │ │ │ ├── name: + │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:6)-(1:17)) + │ │ │ │ └── children: (1 item) + │ │ │ │ └── @ LiteralNode (location: (1:6)-(1:17)) + │ │ │ │ └── content: "crossorigin" + │ │ │ │ + │ │ │ │ + │ │ │ ├── equals: ∅ + │ │ │ └── value: ∅ + │ │ │ + │ │ └── is_void: false + │ │ + │ ├── tag_name: "link" (location: (1:1)-(1:5)) + │ ├── body: [] + │ ├── close_tag: ∅ + │ ├── is_void: true + │ └── source: "HTML" + │ + └── @ ERBContentNode (location: (1:21)-(1:30)) + ├── tag_opening: "<%=" (location: (1:21)-(1:21)) + ├── content: " hello " (location: (1:21)-(1:28)) + ├── tag_closing: "%>" (location: (1:28)-(1:30)) + ├── parsed: true + └── valid: true \ No newline at end of file diff --git a/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt new file mode 100644 index 000000000..3dce31a53 --- /dev/null +++ b/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt @@ -0,0 +1,75 @@ +@ DocumentNode (location: (1:0)-(4:0)) +└── children: (2 items) + ├── @ HTMLElementNode (location: (1:0)-(3:6)) + │ ├── open_tag: + │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:22)) + │ │ ├── tag_opening: "<" (location: (1:0)-(1:1)) + │ │ ├── tag_name: "div" (location: (1:1)-(1:4)) + │ │ ├── tag_closing: ">" (location: (1:21)-(1:22)) + │ │ ├── children: (4 items) + │ │ │ ├── @ WhitespaceNode (location: (1:4)-(1:5)) + │ │ │ │ └── value: " " (location: (1:4)-(1:5)) + │ │ │ │ + │ │ │ ├── @ HTMLAttributeNode (location: (1:5)-(1:14)) + │ │ │ │ ├── name: + │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:5)-(1:7)) + │ │ │ │ │ └── children: (1 item) + │ │ │ │ │ └── @ LiteralNode (location: (1:5)-(1:7)) + │ │ │ │ │ └── content: "id" + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ ├── equals: "=" (location: (1:7)-(1:8)) + │ │ │ │ └── value: + │ │ │ │ └── @ HTMLAttributeValueNode (location: (1:8)-(1:14)) + │ │ │ │ ├── open_quote: """ (location: (1:8)-(1:9)) + │ │ │ │ ├── children: (1 item) + │ │ │ │ │ └── @ LiteralNode (location: (1:9)-(1:13)) + │ │ │ │ │ └── content: "test" + │ │ │ │ │ + │ │ │ │ ├── close_quote: """ (location: (1:13)-(1:14)) + │ │ │ │ └── quoted: true + │ │ │ │ + │ │ │ │ + │ │ │ ├── @ WhitespaceNode (location: (1:14)-(1:15)) + │ │ │ │ └── value: " " (location: (1:14)-(1:15)) + │ │ │ │ + │ │ │ └── @ HTMLAttributeNode (location: (1:15)-(1:21)) + │ │ │ ├── name: + │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:15)-(1:21)) + │ │ │ │ └── children: (1 item) + │ │ │ │ └── @ LiteralNode (location: (1:15)-(1:21)) + │ │ │ │ └── content: "hidden" + │ │ │ │ + │ │ │ │ + │ │ │ ├── equals: ∅ + │ │ │ └── value: ∅ + │ │ │ + │ │ └── is_void: false + │ │ + │ ├── tag_name: "div" (location: (1:1)-(1:4)) + │ ├── body: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:5)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBContentNode (location: (2:2)-(2:13)) + │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5)) + │ │ │ ├── content: " "hi" " (location: (2:5)-(2:11)) + │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13)) + │ │ │ ├── parsed: true + │ │ │ └── valid: true + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:13)-(3:0)) + │ │ └── content: "\n" + │ │ + │ ├── close_tag: + │ │ └── @ HTMLCloseTagNode (location: (3:0)-(3:6)) + │ │ ├── tag_opening: "" (location: (3:5)-(3:6)) + │ │ + │ ├── is_void: false + │ └── source: "HTML" + │ + └── @ HTMLTextNode (location: (3:6)-(4:0)) + └── content: "\n" \ No newline at end of file From c5fb3ee431918037ab7a71fd232184ec4d3b41e0 Mon Sep 17 00:00:00 2001 From: Matt Duszynski Date: Wed, 1 Oct 2025 16:12:30 +0200 Subject: [PATCH 05/97] Linter: Add test case for trailing boolean attribute in HTML element (#485) Sharing my investigation so far - I was able to reproduce the issue reported in #471. The core parser appears to be working correctly, but the linter errors with an unexpected token error. --- .../packages/linter/test/__snapshots__/cli.test.ts.snap | 9 +++++++++ javascript/packages/linter/test/cli.test.ts | 7 +++++++ .../linter/test/fixtures/boolean-attribute.html.erb | 3 +++ 3 files changed, 19 insertions(+) create mode 100644 javascript/packages/linter/test/fixtures/boolean-attribute.html.erb diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index bf976497d..285f873cc 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -743,6 +743,15 @@ exports[`CLI Output Formatting > formats success output correctly 1`] = ` Offenses 0 offenses" `; +exports[`CLI Output Formatting > handles boolean attributes 1`] = ` +"✓ test/fixtures/boolean-attribute.html.erb - No issues found + + + Summary: + Checked 1 file + Offenses 0 offenses" +`; + exports[`CLI Output Formatting > handles multiple errors correctly 1`] = ` "[error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) diff --git a/javascript/packages/linter/test/cli.test.ts b/javascript/packages/linter/test/cli.test.ts index 4d1fa047f..ca39c0489 100644 --- a/javascript/packages/linter/test/cli.test.ts +++ b/javascript/packages/linter/test/cli.test.ts @@ -50,6 +50,13 @@ describe("CLI Output Formatting", () => { expect(exitCode).toBe(1) }) + test("handles boolean attributes", () => { + const { output, exitCode } = runLinter("boolean-attribute.html.erb", "--no-wrap-lines") + + expect(output).toMatchSnapshot() + expect(exitCode).toBe(0) + }) + test("formats success output correctly", () => { const { output, exitCode } = runLinter("clean-file.html.erb", "--no-wrap-lines") diff --git a/javascript/packages/linter/test/fixtures/boolean-attribute.html.erb b/javascript/packages/linter/test/fixtures/boolean-attribute.html.erb new file mode 100644 index 000000000..94811a213 --- /dev/null +++ b/javascript/packages/linter/test/fixtures/boolean-attribute.html.erb @@ -0,0 +1,3 @@ + From 8f9ee4a9d4d0370acf6440d76a0ce9fcd6d335d5 Mon Sep 17 00:00:00 2001 From: Domingo Edwards Date: Wed, 1 Oct 2025 11:34:47 -0300 Subject: [PATCH 06/97] Linter: fix rule generator template (#563) Fix the template for implementing new rules. --- .../linter-rule/generators/app/templates/rule.ts.ejs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/packages/linter/generators/linter-rule/generators/app/templates/rule.ts.ejs b/javascript/packages/linter/generators/linter-rule/generators/app/templates/rule.ts.ejs index 2612d781b..c3a5b3df0 100644 --- a/javascript/packages/linter/generators/linter-rule/generators/app/templates/rule.ts.ejs +++ b/javascript/packages/linter/generators/linter-rule/generators/app/templates/rule.ts.ejs @@ -12,7 +12,7 @@ import { BaseRuleVisitor } from "./rule-utils.js" import { ParserRule } from "../types.js" import type { LintOffense, LintContext } from "../types.js" -import type { Node, HTMLElementNode, ERBContentNode } from "@herb-tools/core" +import type { ParseResult, HTMLElementNode, ERBContentNode } from "@herb-tools/core" class <%= visitorClassName %> extends BaseRuleVisitor { visitHTMLElementNode(node: HTMLElementNode): void { @@ -31,10 +31,10 @@ class <%= visitorClassName %> extends BaseRuleVisitor { export class <%= ruleClassName %> extends ParserRule { name = "<%= ruleName %>" - check(node: Node, context?: Partial): LintOffense[] { + check(result: ParseResult, context?: Partial): LintOffense[] { const visitor = new <%= visitorClassName %>(this.name, context) - visitor.visit(node) + visitor.visit(result.value) return visitor.offenses } From 6239048b44b905eaa438368b10e952657db3044b Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Fri, 3 Oct 2025 05:45:04 +0900 Subject: [PATCH 07/97] Lexer: Support lexing and parsing `=%>` ERB closing tag (#568) This pull request adds support to be able to lex and parse the `=%>` ERB closing tag. Since it's use is quite unknown and not well-defined we should be able to parse is, so we can guide and advice people in the linter to now use it, i.e using the Right Trim rule introduced in #556. Co-Authored-By: Domingo Edwards --- src/include/lexer_peek_helpers.h | 1 + src/lexer.c | 1 + src/lexer_peek_helpers.c | 6 +- test/lexer/erb_test.rb | 8 ++ test/parser/erb_test.rb | 24 ++++ ...%_=%>_344eadf6b1e04a6f534a3c7e38bbadf1.txt | 4 + ...=_=%>_273e03432c2039cc05443a602e8c633a.txt | 4 + ...alue_91d881ce0dd66286e4866c07a91e025c.txt} | 0 ...fore_e474134558bf8ffccb829ba880271d99.txt} | 0 ...fter_4ce9cd67e9f49e97d785cb27fb5e287a.txt} | 0 ...fter_e07f0e9a593189f256789bd0ab7b5a82.txt} | 0 ...tent_3b1dbeebb7d6cc88ee758abcd174c7c4.txt} | 0 ...ruby_71e2451824a84db91b7e1cf752872168.txt} | 0 ..._tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt} | 0 ...e_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt | 8 ++ ...e_tag_040322f44f30d4766dc7bfe98114d53f.txt | 47 +++++++ ...e_tag_276e28a241d24b1f8016400a1c84fef1.txt | 121 ++++++++++++++++++ 17 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 test/snapshots/lexer/erb_test/test_0008_erb_<%_=%>_344eadf6b1e04a6f534a3c7e38bbadf1.txt create mode 100644 test/snapshots/lexer/erb_test/test_0009_erb_<%=_=%>_273e03432c2039cc05443a602e8c633a.txt rename test/snapshots/lexer/erb_test/{test_0008_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt => test_0010_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0009_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt => test_0011_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0010_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt => test_0012_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0011_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt => test_0013_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0012_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt => test_0014_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0013_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt => test_0015_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt} (100%) rename test/snapshots/lexer/erb_test/{test_0014_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt => test_0016_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt} (100%) create mode 100644 test/snapshots/parser/erb_test/test_0033_erb_output_with_=%>_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt create mode 100644 test/snapshots/parser/erb_test/test_0034_erb_if_with_=%>_close_tag_040322f44f30d4766dc7bfe98114d53f.txt create mode 100644 test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%>_close_tag_276e28a241d24b1f8016400a1c84fef1.txt diff --git a/src/include/lexer_peek_helpers.h b/src/include/lexer_peek_helpers.h index ae78bee03..faa0ecec8 100644 --- a/src/include/lexer_peek_helpers.h +++ b/src/include/lexer_peek_helpers.h @@ -31,6 +31,7 @@ bool lexer_peek_for_html_comment_end(const lexer_T* lexer, int offset); bool lexer_peek_erb_close_tag(const lexer_T* lexer, int offset); bool lexer_peek_erb_dash_close_tag(const lexer_T* lexer, int offset); bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, int offset); +bool lexer_peek_erb_equals_close_tag(const lexer_T* lexer, int offset); bool lexer_peek_erb_end(const lexer_T* lexer, int offset); char lexer_backtrack(const lexer_T* lexer, int offset); diff --git a/src/lexer.c b/src/lexer.c index 6e91e180c..513f67882 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -256,6 +256,7 @@ static token_T* lexer_parse_erb_close(lexer_T* lexer) { lexer->state = STATE_DATA; if (lexer_peek_erb_percent_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "%%>", TOKEN_ERB_END); } + if (lexer_peek_erb_equals_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "=%>", TOKEN_ERB_END); } if (lexer_peek_erb_dash_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "-%>", TOKEN_ERB_END); } return lexer_advance_with(lexer, "%>", TOKEN_ERB_END); diff --git a/src/lexer_peek_helpers.c b/src/lexer_peek_helpers.c index 20bccfc19..a46bbeff6 100644 --- a/src/lexer_peek_helpers.c +++ b/src/lexer_peek_helpers.c @@ -65,10 +65,14 @@ bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, const int offset) { return lexer_peek_for(lexer, offset, "%%>", false); } +bool lexer_peek_erb_equals_close_tag(const lexer_T* lexer, const int offset) { + return lexer_peek_for(lexer, offset, "=%>", false); +} + bool lexer_peek_erb_end(const lexer_T* lexer, const int offset) { return ( lexer_peek_erb_close_tag(lexer, offset) || lexer_peek_erb_dash_close_tag(lexer, offset) - || lexer_peek_erb_percent_close_tag(lexer, offset) + || lexer_peek_erb_percent_close_tag(lexer, offset) || lexer_peek_erb_equals_close_tag(lexer, offset) ); } diff --git a/test/lexer/erb_test.rb b/test/lexer/erb_test.rb index b3c5b1ded..e821c7e8e 100644 --- a/test/lexer/erb_test.rb +++ b/test/lexer/erb_test.rb @@ -34,6 +34,14 @@ class ERBTest < Minitest::Spec assert_lexed_snapshot(%(<%%= "Test" %%>)) end + test "erb <% =%>" do + assert_lexed_snapshot(%(<% "Test" =%>)) + end + + test "erb <%= =%>" do + assert_lexed_snapshot(%(<%= "Test" =%>)) + end + test "erb output inside HTML attribute value" do assert_lexed_snapshot(%(
)) end diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb index 1c95ef3f6..142a25eab 100644 --- a/test/parser/erb_test.rb +++ b/test/parser/erb_test.rb @@ -181,5 +181,29 @@ class ERBTest < Minitest::Spec %> HTML end + + test "erb output with =%> close tag" do + assert_parsed_snapshot(%(<%= "hello" =%>)) + end + + test "erb if with =%> close tag" do + assert_parsed_snapshot(<<~HTML) + <% if true =%> +

Content

+ <% end =%> + HTML + end + + test "erb if-elsif-else with =%> close tag" do + assert_parsed_snapshot(<<~HTML) + <% if condition =%> +

True

+ <% elsif other =%> +

Other

+ <% else =%> +

False

+ <% end =%> + HTML + end end end diff --git a/test/snapshots/lexer/erb_test/test_0008_erb_<%_=%>_344eadf6b1e04a6f534a3c7e38bbadf1.txt b/test/snapshots/lexer/erb_test/test_0008_erb_<%_=%>_344eadf6b1e04a6f534a3c7e38bbadf1.txt new file mode 100644 index 000000000..f84f7c4c9 --- /dev/null +++ b/test/snapshots/lexer/erb_test/test_0008_erb_<%_=%>_344eadf6b1e04a6f534a3c7e38bbadf1.txt @@ -0,0 +1,4 @@ +# +# +# +# diff --git a/test/snapshots/lexer/erb_test/test_0009_erb_<%=_=%>_273e03432c2039cc05443a602e8c633a.txt b/test/snapshots/lexer/erb_test/test_0009_erb_<%=_=%>_273e03432c2039cc05443a602e8c633a.txt new file mode 100644 index 000000000..62d751f5b --- /dev/null +++ b/test/snapshots/lexer/erb_test/test_0009_erb_<%=_=%>_273e03432c2039cc05443a602e8c633a.txt @@ -0,0 +1,4 @@ +# +# +# +# diff --git a/test/snapshots/lexer/erb_test/test_0008_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt b/test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0008_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt rename to test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt diff --git a/test/snapshots/lexer/erb_test/test_0009_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt b/test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0009_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt rename to test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt diff --git a/test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt b/test/snapshots/lexer/erb_test/test_0012_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt rename to test/snapshots/lexer/erb_test/test_0012_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt diff --git a/test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt b/test/snapshots/lexer/erb_test/test_0013_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt rename to test/snapshots/lexer/erb_test/test_0013_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt diff --git a/test/snapshots/lexer/erb_test/test_0012_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt b/test/snapshots/lexer/erb_test/test_0014_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0012_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt rename to test/snapshots/lexer/erb_test/test_0014_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt diff --git a/test/snapshots/lexer/erb_test/test_0013_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt b/test/snapshots/lexer/erb_test/test_0015_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0013_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt rename to test/snapshots/lexer/erb_test/test_0015_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt diff --git a/test/snapshots/lexer/erb_test/test_0014_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt b/test/snapshots/lexer/erb_test/test_0016_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt similarity index 100% rename from test/snapshots/lexer/erb_test/test_0014_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt rename to test/snapshots/lexer/erb_test/test_0016_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt diff --git a/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%>_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt b/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%>_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt new file mode 100644 index 000000000..06e906b7b --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%>_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt @@ -0,0 +1,8 @@ +@ DocumentNode (location: (1:0)-(1:15)) +└── children: (1 item) + └── @ ERBContentNode (location: (1:0)-(1:15)) + ├── tag_opening: "<%=" (location: (1:0)-(1:3)) + ├── content: " "hello" " (location: (1:3)-(1:12)) + ├── tag_closing: "=%>" (location: (1:12)-(1:15)) + ├── parsed: true + └── valid: true \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%>_close_tag_040322f44f30d4766dc7bfe98114d53f.txt b/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%>_close_tag_040322f44f30d4766dc7bfe98114d53f.txt new file mode 100644 index 000000000..5ac9012b3 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%>_close_tag_040322f44f30d4766dc7bfe98114d53f.txt @@ -0,0 +1,47 @@ +@ DocumentNode (location: (1:0)-(4:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(3:10)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if true " (location: (1:2)-(1:11)) + │ ├── tag_closing: "=%>" (location: (1:11)-(1:14)) + │ ├── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:14)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ HTMLElementNode (location: (2:2)-(2:16)) + │ │ │ ├── open_tag: + │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:5)) + │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3)) + │ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4)) + │ │ │ │ ├── tag_closing: ">" (location: (2:4)-(2:5)) + │ │ │ │ ├── children: [] + │ │ │ │ └── is_void: false + │ │ │ │ + │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (2:5)-(2:12)) + │ │ │ │ └── content: "Content" + │ │ │ │ + │ │ │ ├── close_tag: + │ │ │ │ └── @ HTMLCloseTagNode (location: (2:12)-(2:16)) + │ │ │ │ ├── tag_opening: "" (location: (2:15)-(2:16)) + │ │ │ │ + │ │ │ ├── is_void: false + │ │ │ └── source: "HTML" + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:16)-(3:0)) + │ │ └── content: "\n" + │ │ + │ ├── subsequent: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (3:0)-(3:10)) + │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ ├── content: " end " (location: (3:2)-(3:7)) + │ └── tag_closing: "=%>" (location: (3:7)-(3:10)) + │ + │ + └── @ HTMLTextNode (location: (3:10)-(4:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%>_close_tag_276e28a241d24b1f8016400a1c84fef1.txt b/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%>_close_tag_276e28a241d24b1f8016400a1c84fef1.txt new file mode 100644 index 000000000..8c281e427 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%>_close_tag_276e28a241d24b1f8016400a1c84fef1.txt @@ -0,0 +1,121 @@ +@ DocumentNode (location: (1:0)-(8:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(7:10)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if condition " (location: (1:2)-(1:16)) + │ ├── tag_closing: "=%>" (location: (1:16)-(1:19)) + │ ├── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:19)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ HTMLElementNode (location: (2:2)-(2:13)) + │ │ │ ├── open_tag: + │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:5)) + │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3)) + │ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4)) + │ │ │ │ ├── tag_closing: ">" (location: (2:4)-(2:5)) + │ │ │ │ ├── children: [] + │ │ │ │ └── is_void: false + │ │ │ │ + │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (2:5)-(2:9)) + │ │ │ │ └── content: "True" + │ │ │ │ + │ │ │ ├── close_tag: + │ │ │ │ └── @ HTMLCloseTagNode (location: (2:9)-(2:13)) + │ │ │ │ ├── tag_opening: "" (location: (2:12)-(2:13)) + │ │ │ │ + │ │ │ ├── is_void: false + │ │ │ └── source: "HTML" + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:13)-(3:0)) + │ │ └── content: "\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBIfNode (location: (3:0)-(5:0)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " elsif other " (location: (3:2)-(3:15)) + │ │ ├── tag_closing: "=%>" (location: (3:15)-(3:18)) + │ │ ├── statements: (3 items) + │ │ │ ├── @ HTMLTextNode (location: (3:18)-(4:2)) + │ │ │ │ └── content: "\n " + │ │ │ │ + │ │ │ ├── @ HTMLElementNode (location: (4:2)-(4:14)) + │ │ │ │ ├── open_tag: + │ │ │ │ │ └── @ HTMLOpenTagNode (location: (4:2)-(4:5)) + │ │ │ │ │ ├── tag_opening: "<" (location: (4:2)-(4:3)) + │ │ │ │ │ ├── tag_name: "p" (location: (4:3)-(4:4)) + │ │ │ │ │ ├── tag_closing: ">" (location: (4:4)-(4:5)) + │ │ │ │ │ ├── children: [] + │ │ │ │ │ └── is_void: false + │ │ │ │ │ + │ │ │ │ ├── tag_name: "p" (location: (4:3)-(4:4)) + │ │ │ │ ├── body: (1 item) + │ │ │ │ │ └── @ HTMLTextNode (location: (4:5)-(4:10)) + │ │ │ │ │ └── content: "Other" + │ │ │ │ │ + │ │ │ │ ├── close_tag: + │ │ │ │ │ └── @ HTMLCloseTagNode (location: (4:10)-(4:14)) + │ │ │ │ │ ├── tag_opening: "" (location: (4:13)-(4:14)) + │ │ │ │ │ + │ │ │ │ ├── is_void: false + │ │ │ │ └── source: "HTML" + │ │ │ │ + │ │ │ └── @ HTMLTextNode (location: (4:14)-(5:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ ├── subsequent: + │ │ │ └── @ ERBElseNode (location: (5:0)-(7:0)) + │ │ │ ├── tag_opening: "<%" (location: (5:0)-(5:2)) + │ │ │ ├── content: " else " (location: (5:2)-(5:8)) + │ │ │ ├── tag_closing: "=%>" (location: (5:8)-(5:11)) + │ │ │ └── statements: (3 items) + │ │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2)) + │ │ │ │ └── content: "\n " + │ │ │ │ + │ │ │ ├── @ HTMLElementNode (location: (6:2)-(6:14)) + │ │ │ │ ├── open_tag: + │ │ │ │ │ └── @ HTMLOpenTagNode (location: (6:2)-(6:5)) + │ │ │ │ │ ├── tag_opening: "<" (location: (6:2)-(6:3)) + │ │ │ │ │ ├── tag_name: "p" (location: (6:3)-(6:4)) + │ │ │ │ │ ├── tag_closing: ">" (location: (6:4)-(6:5)) + │ │ │ │ │ ├── children: [] + │ │ │ │ │ └── is_void: false + │ │ │ │ │ + │ │ │ │ ├── tag_name: "p" (location: (6:3)-(6:4)) + │ │ │ │ ├── body: (1 item) + │ │ │ │ │ └── @ HTMLTextNode (location: (6:5)-(6:10)) + │ │ │ │ │ └── content: "False" + │ │ │ │ │ + │ │ │ │ ├── close_tag: + │ │ │ │ │ └── @ HTMLCloseTagNode (location: (6:10)-(6:14)) + │ │ │ │ │ ├── tag_opening: "" (location: (6:13)-(6:14)) + │ │ │ │ │ + │ │ │ │ ├── is_void: false + │ │ │ │ └── source: "HTML" + │ │ │ │ + │ │ │ └── @ HTMLTextNode (location: (6:14)-(7:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ │ + │ │ └── end_node: ∅ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (7:0)-(7:10)) + │ ├── tag_opening: "<%" (location: (7:0)-(7:2)) + │ ├── content: " end " (location: (7:2)-(7:7)) + │ └── tag_closing: "=%>" (location: (7:7)-(7:10)) + │ + │ + └── @ HTMLTextNode (location: (7:10)-(8:0)) + └── content: "\n" \ No newline at end of file From 8c1718839bc28f031aa2bb3d9953bd4cb7467dde Mon Sep 17 00:00:00 2001 From: Domingo Edwards Date: Thu, 2 Oct 2025 18:43:22 -0300 Subject: [PATCH 08/97] Linter: Implement `erb-right-trim` linter rule (#556) Solves #551 Implements the [RightTrim rule from erb-lint](https://github.com/Shopify/erb_lint?tab=readme-ov-file#righttrim). --- .../packages/linter/docs/rules/README.md | 1 + .../linter/docs/rules/erb-right-trim.md | 23 +++++++++ .../packages/linter/src/default-rules.ts | 2 + .../linter/src/rules/erb-right-trim.ts | 38 ++++++++++++++ javascript/packages/linter/src/rules/index.ts | 1 + .../test/__snapshots__/cli.test.ts.snap | 6 +-- .../linter/test/rules/erb-right-trim.test.ts | 51 +++++++++++++++++++ javascript/packages/vscode/package.json | 1 + 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 javascript/packages/linter/docs/rules/erb-right-trim.md create mode 100644 javascript/packages/linter/src/rules/erb-right-trim.ts create mode 100644 javascript/packages/linter/test/rules/erb-right-trim.test.ts diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md index 4a361e7ec..625d62d94 100644 --- a/javascript/packages/linter/docs/rules/README.md +++ b/javascript/packages/linter/docs/rules/README.md @@ -11,6 +11,7 @@ This page contains documentation for all Herb Linter rules. - [`erb-prefer-image-tag-helper`](./erb-prefer-image-tag-helper.md) - Prefer `image_tag` helper over `` with ERB expressions - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags - [`erb-requires-trailing-newline`](./erb-requires-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character. +- [`erb-right-trim`](./erb-right-trim.md) - Enforces trimming with `-` - [`html-anchor-require-href`](./html-anchor-require-href.md) - Requires an href attribute on anchor tags - [`html-aria-attribute-must-be-valid`](./html-aria-attribute-must-be-valid.md) - Disallow invalid or unknown `aria-*` attributes. - [`html-aria-label-is-well-formatted`](./html-aria-label-is-well-formatted.md) - `aria-label` must be well-formatted diff --git a/javascript/packages/linter/docs/rules/erb-right-trim.md b/javascript/packages/linter/docs/rules/erb-right-trim.md new file mode 100644 index 000000000..b44861994 --- /dev/null +++ b/javascript/packages/linter/docs/rules/erb-right-trim.md @@ -0,0 +1,23 @@ +# Linter Rule: Enforces trimming with - + +**Rule:**: `erb-right-trim` + +Trimming at the right of an ERB tag can be done with either =%> or -%>, this linter enforces "-" as the default trimming style. + +## Examples + +### ❌ Incorrect + +```erb +<%= title =%> +``` + +### ✅ Correct + +```erb +<%= title -%> +``` + +## Configuration + +This rule has no configuration options. diff --git a/javascript/packages/linter/src/default-rules.ts b/javascript/packages/linter/src/default-rules.ts index 8dcd84e9f..3126bc3b4 100644 --- a/javascript/packages/linter/src/default-rules.ts +++ b/javascript/packages/linter/src/default-rules.ts @@ -35,6 +35,7 @@ import { HTMLTagNameLowercaseRule } from "./rules/html-tag-name-lowercase.js" import { ParserNoErrorsRule } from "./rules/parser-no-errors.js" import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js" import { HTMLNoUnderscoresInAttributeNamesRule } from "./rules/html-no-underscores-in-attribute-names.js" +import { ERBRightTrimRule } from "./rules/erb-right-trim.js" export const defaultRules: RuleClass[] = [ ERBCommentSyntax, @@ -72,4 +73,5 @@ export const defaultRules: RuleClass[] = [ ParserNoErrorsRule, SVGTagNameCapitalizationRule, HTMLNoUnderscoresInAttributeNamesRule, + ERBRightTrimRule, ] diff --git a/javascript/packages/linter/src/rules/erb-right-trim.ts b/javascript/packages/linter/src/rules/erb-right-trim.ts new file mode 100644 index 000000000..ccd3083e3 --- /dev/null +++ b/javascript/packages/linter/src/rules/erb-right-trim.ts @@ -0,0 +1,38 @@ +import { BaseRuleVisitor } from "./rule-utils.js" +import { ParserRule } from "../types.js" + +import type { LintOffense, LintContext } from "../types.js" +import type { ERBContentNode, ParseResult } from "@herb-tools/core" + +class ERBRightTrimVisitor extends BaseRuleVisitor { + + visitERBContentNode(node: ERBContentNode): void { + if (!node.tag_closing) { + return + } + + if (!node.content?.value) { + return + } + + if (node.tag_closing.value === "=%>") { + this.addOffense( + "Prefer -%> instead of =%> for trimming on the right", + node.location + ) + } + } + +} + +export class ERBRightTrimRule extends ParserRule { + name = "erb-right-trim" + + check(result: ParseResult, context?: Partial): LintOffense[] { + const visitor = new ERBRightTrimVisitor(this.name, context) + + visitor.visit(result.value) + + return visitor.offenses + } +} diff --git a/javascript/packages/linter/src/rules/index.ts b/javascript/packages/linter/src/rules/index.ts index ce38e34f0..92b7d97e1 100644 --- a/javascript/packages/linter/src/rules/index.ts +++ b/javascript/packages/linter/src/rules/index.ts @@ -31,3 +31,4 @@ export * from "./html-no-title-attribute.js" export * from "./html-tag-name-lowercase.js" export * from "./svg-tag-name-capitalization.js" export * from "./html-no-underscores-in-attribute-names.js" +export * from "./erb-right-trim.js" diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index 285f873cc..d24c9fd66 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -561,7 +561,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for bad file 1`] "summary": { "filesChecked": 1, "filesWithOffenses": 1, - "ruleCount": 32, + "ruleCount": 33, "totalErrors": 2, "totalOffenses": 2, "totalWarnings": 0, @@ -579,7 +579,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for clean file 1` "summary": { "filesChecked": 1, "filesWithOffenses": 0, - "ruleCount": 32, + "ruleCount": 33, "totalErrors": 0, "totalOffenses": 0, "totalWarnings": 0, @@ -649,7 +649,7 @@ exports[`CLI Output Formatting > formats JSON output correctly for file with err "summary": { "filesChecked": 1, "filesWithOffenses": 1, - "ruleCount": 32, + "ruleCount": 33, "totalErrors": 3, "totalOffenses": 3, "totalWarnings": 0, diff --git a/javascript/packages/linter/test/rules/erb-right-trim.test.ts b/javascript/packages/linter/test/rules/erb-right-trim.test.ts new file mode 100644 index 000000000..c38b5b76b --- /dev/null +++ b/javascript/packages/linter/test/rules/erb-right-trim.test.ts @@ -0,0 +1,51 @@ +import dedent from "dedent" +import { describe, test, expect, beforeAll } from "vitest" +import { Herb } from "@herb-tools/node-wasm" +import { Linter } from "../../src/linter.js" + +import { ERBRightTrimRule } from "../../src/rules/erb-right-trim.js" + +describe("ERBRightTrimRule", () => { + beforeAll(async () => { + await Herb.load() + }) + + test("when the erb tag close with %>", () => { + const html = dedent` +

+ <%= title %> +

+ ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(0) + }) + + test("when the erb tag close with -%>", () => { + const html = dedent` +

+ <%= title -%> +

+ ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.offenses).toHaveLength(0) + }) + + test("when the erb tag close with =%>", () => { + const html = dedent` +

+ <%= title =%> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(1) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(1) + expect(lintResult.offenses[0].code).toBe("erb-right-trim") + expect(lintResult.offenses[0].message).toBe("Prefer -%> instead of =%> for trimming on the right") + }) +}) diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json index 571a05a17..63036b345 100644 --- a/javascript/packages/vscode/package.json +++ b/javascript/packages/vscode/package.json @@ -68,6 +68,7 @@ "erb-prefer-image-tag-helper", "erb-require-whitespace-inside-tags", "erb-requires-trailing-newline", + "erb-right-trim", "html-anchor-require-href", "html-aria-attribute-must-be-valid", "html-aria-label-is-well-formatted", From 69a4d9761b32f21e38d55356fcaf4e32a513ffcb Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Fri, 3 Oct 2025 07:19:42 +0900 Subject: [PATCH 09/97] Add `visitNode` and `visitERBNode` methods to improve `erb-right-trim` (#569) This pull request introduces two new methods `visitNode(node: Node)` and `visitERBNode(node: ERBNode)` in the JavaScript visitor that allows to visit any node, or visit any ERB node. This is useful and allows us to improve the `erb-right-trim` introduced in #556. This now updates the `erb-right-trim` to also handle cases where the right trimming is used when it has no effect (like on non-outputting ERB Nodes like `<%`). /cc @domingo2000 --- .../packages/linter/docs/rules/README.md | 2 +- .../linter/docs/rules/erb-right-trim.md | 52 ++++++++-- .../linter/src/rules/erb-right-trim.ts | 28 +++--- .../packages/linter/src/rules/rule-utils.ts | 4 +- .../linter/test/rules/erb-right-trim.test.ts | 98 ++++++++++++++++++- .../packages/core/src/visitor.ts.erb | 30 +++++- 6 files changed, 189 insertions(+), 25 deletions(-) diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md index 625d62d94..883a73b6a 100644 --- a/javascript/packages/linter/docs/rules/README.md +++ b/javascript/packages/linter/docs/rules/README.md @@ -11,7 +11,7 @@ This page contains documentation for all Herb Linter rules. - [`erb-prefer-image-tag-helper`](./erb-prefer-image-tag-helper.md) - Prefer `image_tag` helper over `` with ERB expressions - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags - [`erb-requires-trailing-newline`](./erb-requires-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character. -- [`erb-right-trim`](./erb-right-trim.md) - Enforces trimming with `-` +- [`erb-right-trim`](./erb-right-trim.md) - Enforce consistent right-trimming syntax. - [`html-anchor-require-href`](./html-anchor-require-href.md) - Requires an href attribute on anchor tags - [`html-aria-attribute-must-be-valid`](./html-aria-attribute-must-be-valid.md) - Disallow invalid or unknown `aria-*` attributes. - [`html-aria-label-is-well-formatted`](./html-aria-label-is-well-formatted.md) - `aria-label` must be well-formatted diff --git a/javascript/packages/linter/docs/rules/erb-right-trim.md b/javascript/packages/linter/docs/rules/erb-right-trim.md index b44861994..74901eed9 100644 --- a/javascript/packages/linter/docs/rules/erb-right-trim.md +++ b/javascript/packages/linter/docs/rules/erb-right-trim.md @@ -1,23 +1,57 @@ -# Linter Rule: Enforces trimming with - +# Linter Rule: Enforce consistent right-trimming syntax -**Rule:**: `erb-right-trim` +**Rule:** `erb-right-trim` -Trimming at the right of an ERB tag can be done with either =%> or -%>, this linter enforces "-" as the default trimming style. +## Description + +This rule enforces the use of `-%>` for right-trimming ERB output tags (like `<%= %>`) instead of `=%>`. It also warns when right-trimming syntax (`-%>` or `=%>`) is used on non-output ERB tags (like `<% %>`, `<% if %>`, etc.) where it has no effect. + +## Rationale + +While `=%>` can be used for right-trimming whitespace in some ERB engines (like Erubi), it is an obscure and not well-defined syntax that lacks consistent support across most ERB implementations. The `-%>` syntax is the standard, well-documented approach for right-trimming that is universally supported and consistent with left-trimming syntax (`<%-`). + +Additionally, right-trimming syntax only has an effect on ERB output tags (`<%=` and `<%==`). Using `-%>` or `=%>` on non-output ERB tags (control flow like `<% if %>`, `<% each %>`, etc.) has no effect and is misleading. + +Using `-%>` for output tags ensures compatibility across different ERB engines, improves code clarity, and aligns with established Rails and ERB conventions. ## Examples -### ❌ Incorrect +### ✅ Good ```erb -<%= title =%> +<%= title -%> + +<% if true %> +

Content

+<% end %> + +<% items.each do |item| %> +
  • <%= item -%>
  • +<% end %> ``` -### ✅ Correct +### 🚫 Bad ```erb -<%= title -%> +<%= title =%> + + +<% title =%> + + +<% title -%> + + +<% if true -%> +

    Content

    +<% end %> + + +<% items.each do |item| =%> +
  • <%= item %>
  • +<% end %> ``` -## Configuration +## References -This rule has no configuration options. +- [Inspiration: ERB Lint `RightTrim` rule](https://github.com/Shopify/erb_lint/blob/main/README.md#righttrim) diff --git a/javascript/packages/linter/src/rules/erb-right-trim.ts b/javascript/packages/linter/src/rules/erb-right-trim.ts index ccd3083e3..182db6857 100644 --- a/javascript/packages/linter/src/rules/erb-right-trim.ts +++ b/javascript/packages/linter/src/rules/erb-right-trim.ts @@ -1,28 +1,34 @@ import { BaseRuleVisitor } from "./rule-utils.js" import { ParserRule } from "../types.js" +import { isERBOutputNode } from "@herb-tools/core" import type { LintOffense, LintContext } from "../types.js" -import type { ERBContentNode, ParseResult } from "@herb-tools/core" +import type { ERBNode, ParseResult } from "@herb-tools/core" class ERBRightTrimVisitor extends BaseRuleVisitor { - - visitERBContentNode(node: ERBContentNode): void { - if (!node.tag_closing) { - return - } + visitERBNode(node: ERBNode): void { + if (!node.tag_closing) return + + const trimClosing = node.tag_closing.value + + if (trimClosing !== "=%>" && trimClosing !== "-%>") return + + if (!isERBOutputNode(node)) { + this.addOffense( + `Right-trimming with \`${trimClosing}\` has no effect on non-output ERB tags. Use \`%>\` instead`, + node.tag_closing.location + ) - if (!node.content?.value) { return } - if (node.tag_closing.value === "=%>") { + if (trimClosing === "=%>") { this.addOffense( - "Prefer -%> instead of =%> for trimming on the right", - node.location + "Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines", + node.tag_closing.location ) } } - } export class ERBRightTrimRule extends ParserRule { diff --git a/javascript/packages/linter/src/rules/rule-utils.ts b/javascript/packages/linter/src/rules/rule-utils.ts index 911b16783..ae29bc634 100644 --- a/javascript/packages/linter/src/rules/rule-utils.ts +++ b/javascript/packages/linter/src/rules/rule-utils.ts @@ -13,7 +13,7 @@ import { } from "@herb-tools/core" import type { - ERBNode, + ERBContentNode, HTMLAttributeNameNode, HTMLAttributeNode, HTMLAttributeValueNode, @@ -300,7 +300,7 @@ export function getAttributeValue(attributeNode: HTMLAttributeNode): string | nu for (const child of valueNode.children) { switch (child.type) { case "AST_ERB_CONTENT_NODE": { - const erbNode = child as ERBNode + const erbNode = child as ERBContentNode if (erbNode.content) { result += `${erbNode.tag_opening?.value}${erbNode.content.value}${erbNode.tag_closing?.value}` diff --git a/javascript/packages/linter/test/rules/erb-right-trim.test.ts b/javascript/packages/linter/test/rules/erb-right-trim.test.ts index c38b5b76b..3e97321a4 100644 --- a/javascript/packages/linter/test/rules/erb-right-trim.test.ts +++ b/javascript/packages/linter/test/rules/erb-right-trim.test.ts @@ -46,6 +46,102 @@ describe("ERBRightTrimRule", () => { expect(lintResult.warnings).toBe(0) expect(lintResult.offenses).toHaveLength(1) expect(lintResult.offenses[0].code).toBe("erb-right-trim") - expect(lintResult.offenses[0].message).toBe("Prefer -%> instead of =%> for trimming on the right") + expect(lintResult.offenses[0].message).toBe("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + }) + + test("when an if block uses =%>", () => { + const html = dedent` + <% if condition =%> +

    Content

    + <% end %> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(1) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(1) + expect(lintResult.offenses[0].code).toBe("erb-right-trim") + expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + }) + + test("when an if-else block uses =%>", () => { + const html = dedent` + <% if condition =%> +

    True branch

    + <% else =%> +

    False branch

    + <% end %> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(2) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(2) + expect(lintResult.offenses[0].code).toBe("erb-right-trim") + + expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[1].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + }) + + test("when each block uses =%>", () => { + const html = dedent` + <% items.each do |item| =%> +
  • <%= item %>
  • + <% end %> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(1) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(1) + expect(lintResult.offenses[0].code).toBe("erb-right-trim") + expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + }) + + test("when non-output tag uses -%>", () => { + const html = dedent` + <% if condition -%> +

    Content

    + <% elsif other_condition -%> +

    Content

    + <% elsif yet_another_condition -%> +

    Content

    + <% else -%> +

    Content

    + <% end -%> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(5) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(5) + + expect(lintResult.offenses[0].code).toBe("erb-right-trim") + expect(lintResult.offenses[0].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[1].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[2].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[3].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[4].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + }) + + test("when multiple non-output tags use trimming", () => { + const html = dedent` + <% items.each do |item| -%> +
  • <%= item %>
  • + <% end -%> + ` + const linter = new Linter(Herb, [ERBRightTrimRule]) + const lintResult = linter.lint(html) + + expect(lintResult.errors).toBe(2) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(2) + + expect(lintResult.offenses[0].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expect(lintResult.offenses[1].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") }) }) diff --git a/templates/javascript/packages/core/src/visitor.ts.erb b/templates/javascript/packages/core/src/visitor.ts.erb index 47a7cfade..f38a060fc 100644 --- a/templates/javascript/packages/core/src/visitor.ts.erb +++ b/templates/javascript/packages/core/src/visitor.ts.erb @@ -1,11 +1,27 @@ import { Node, + ERBNode, <%- nodes.each do |node| -%> <%= node.name %>, <%- end -%> } from "./nodes.js" -export class Visitor { +/** + * Interface that enforces all node visit methods are implemented + * This ensures that any class implementing IVisitor must have a visit method for every node type + */ +export interface IVisitor { + visit(node: Node | null | undefined): void + visitAll(nodes: (Node | null | undefined)[]): void + visitChildNodes(node: Node): void + <%- nodes.each do |node| -%> + visit<%= node.name %>(node: <%= node.name %>): void + <%- end -%> + visitNode(node: Node): void + visitERBNode(node: ERBNode): void +} + +export class Visitor implements IVisitor { visit(node: Node | null | undefined): void { if (!node) return @@ -20,8 +36,20 @@ export class Visitor { node.compactChildNodes().forEach(node => node.accept(this)) } + visitNode(_node: Node): void { + // Default implementation does nothing + } + + visitERBNode(_node: ERBNode): void { + // Default implementation does nothing + } + <%- nodes.each do |node| -%> visit<%= node.name %>(node: <%= node.name %>): void { + this.visitNode(node) + <%- if node.name.start_with?("ERB") -%> + this.visitERBNode(node) + <%- end -%> this.visitChildNodes(node) } From 0e028d1b45b9ed8c5fe0bfd7fac40f7bbcdd3927 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Fri, 3 Oct 2025 07:29:18 +0900 Subject: [PATCH 10/97] Linter Rule: Refactor `erb-require-whitespace-inside-tags` to use `visitERBNode` (#570) This pull request updates the `erb-require-whitespace-inside-tags` linter rule to use the new `visitERBNode` method in the visitor introduced in #569. --- .../rules/erb-require-whitespace-inside-tags.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/javascript/packages/linter/src/rules/erb-require-whitespace-inside-tags.ts b/javascript/packages/linter/src/rules/erb-require-whitespace-inside-tags.ts index 3c5b1c896..05021794f 100644 --- a/javascript/packages/linter/src/rules/erb-require-whitespace-inside-tags.ts +++ b/javascript/packages/linter/src/rules/erb-require-whitespace-inside-tags.ts @@ -1,20 +1,12 @@ -import type { ParseResult, Token, Node } from "@herb-tools/core" -import { isERBNode } from "@herb-tools/core"; import { ParserRule } from "../types.js" -import type { LintOffense, LintContext } from "../types.js" import { BaseRuleVisitor } from "./rule-utils.js" -class RequireWhitespaceInsideTags extends BaseRuleVisitor { +import type { LintOffense, LintContext } from "../types.js" +import type { ParseResult, Token, ERBNode } from "@herb-tools/core" - visitChildNodes(node: Node): void { - this.checkWhitespace(node) - super.visitChildNodes(node) - } +class RequireWhitespaceInsideTags extends BaseRuleVisitor { - private checkWhitespace(node: Node): void { - if (!isERBNode(node)) { - return - } + visitERBNode(node: ERBNode): void { const openTag = node.tag_opening const closeTag = node.tag_closing const content = node.content From edf93582f7a923b31ee28f48c791f466a4a1b8f8 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 4 Oct 2025 09:16:21 +0900 Subject: [PATCH 11/97] docs: Add References section to `erb-comment-syntax` linter rule Signed-off-by: Marco Roth --- javascript/packages/linter/docs/rules/erb-comment-syntax.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/javascript/packages/linter/docs/rules/erb-comment-syntax.md b/javascript/packages/linter/docs/rules/erb-comment-syntax.md index a4891b51f..b3c9ae781 100644 --- a/javascript/packages/linter/docs/rules/erb-comment-syntax.md +++ b/javascript/packages/linter/docs/rules/erb-comment-syntax.md @@ -38,3 +38,7 @@ For multi-line comments or actual Ruby code with comments, ensure the content st <%== # This should also be an ERB comment %> ``` + +## References + +- [Inspiration: ERB Lint `CommentSyntax` rule](https://github.com/shopify/erb_lint?tab=readme-ov-file#commentsyntax) From abaeeb77bbf9ae60af847d1f7884a5b6d9226e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= <3810945+timkaechele@users.noreply.github.com> Date: Sun, 5 Oct 2025 06:44:35 +0200 Subject: [PATCH 12/97] C: Localize token struct members (#529) ## What it does This PR removes the need for heap allocating the struct members of a token (position, range, location) ## How it does it - Change the `token_T` members `range` and `location` to be structs, instead of pointers to structs - Change `location_T` members `start` and `end` to be structs, instead of pointers to structs - Removes functions only used to access struct members, as they were not consistently used anyway - Removes init functions that do not add anything beyond providing a 1-1 mapping of argument to struct members - Removes copy methods as we are passing the structs by value - Use 32bit unsigned integers for range/position/location struct members, effectively limiting the parseable filesize to 2^32-1 bytes (4gb) which for all intents and purposes (templates after all) should more than suffice. Saves a bit of memory without any real world drawbacks. --- config.yml | 28 +-- ext/herb/extension_helpers.c | 24 +- ext/herb/extension_helpers.h | 6 +- javascript/packages/node/binding.gyp | 1 - .../node/extension/extension_helpers.cpp | 36 +-- .../node/extension/extension_helpers.h | 6 +- src/analyze.c | 78 +++---- src/ast_node.c | 23 +- src/include/ast_node.h | 6 +- src/include/lexer_peek_helpers.h | 13 +- src/include/lexer_struct.h | 19 +- src/include/location.h | 23 +- src/include/parser_helpers.h | 2 +- src/include/position.h | 17 +- src/include/pretty_print.h | 2 +- src/include/prism_helpers.h | 2 +- src/include/range.h | 17 +- src/include/token.h | 3 - src/include/token_struct.h | 4 +- src/lexer.c | 4 +- src/lexer_peek_helpers.c | 6 +- src/location.c | 46 +--- src/parser.c | 217 ++++++++---------- src/parser_helpers.c | 30 +-- src/position.c | 33 --- src/pretty_print.c | 19 +- src/prism_helpers.c | 14 +- src/range.c | 37 +-- src/token.c | 54 ++--- templates/src/ast_nodes.c.erb | 4 +- templates/src/errors.c.erb | 12 +- templates/src/include/ast_nodes.h.erb | 4 +- templates/src/include/errors.h.erb | 6 +- wasm/extension_helpers.cpp | 30 +-- wasm/extension_helpers.h | 6 +- 35 files changed, 320 insertions(+), 512 deletions(-) delete mode 100644 src/position.c diff --git a/config.yml b/config.yml index b67bfb480..df26a43fa 100644 --- a/config.yml +++ b/config.yml @@ -34,8 +34,8 @@ errors: arguments: - token_type_to_string(found->type) - token_type_to_string(expected_type) - - found->location->start->line - - found->location->start->column + - found->location.start.line + - found->location.start.column fields: - name: expected_type @@ -49,8 +49,8 @@ errors: template: "Found closing tag `` at (%zu:%zu) without a matching opening tag." arguments: - closing_tag->value - - closing_tag->location->start->line - - closing_tag->location->start->column + - closing_tag->location.start.line + - closing_tag->location.start.column fields: - name: closing_tag @@ -61,8 +61,8 @@ errors: template: "Opening tag `<%s>` at (%zu:%zu) doesn't have a matching closing tag ``." arguments: - opening_tag->value - - opening_tag->location->start->line - - opening_tag->location->start->column + - opening_tag->location.start.line + - opening_tag->location.start.column - opening_tag->value fields: @@ -74,11 +74,11 @@ errors: template: "Opening tag `<%s>` at (%zu:%zu) closed with `` at (%zu:%zu)." arguments: - opening_tag->value - - opening_tag->location->start->line - - opening_tag->location->start->column + - opening_tag->location.start.line + - opening_tag->location.start.column - closing_tag->value - - closing_tag->location->start->line - - closing_tag->location->start->column + - closing_tag->location.start.line + - closing_tag->location.start.column fields: - name: opening_tag @@ -93,8 +93,8 @@ errors: arguments: - opening_quote->value - closing_quote->value - - closing_quote->location->start->line - - closing_quote->location->start->column + - closing_quote->location.start.line + - closing_quote->location.start.column fields: - name: opening_quote @@ -127,8 +127,8 @@ errors: template: "Tag `<%s>` opened at (%zu:%zu) was never closed before the end of document." arguments: - opening_tag->value - - opening_tag->location->start->line - - opening_tag->location->start->column + - opening_tag->location.start.line + - opening_tag->location.start.column fields: - name: opening_tag diff --git a/ext/herb/extension_helpers.c b/ext/herb/extension_helpers.c index 875f34d1f..e9ab162b5 100644 --- a/ext/herb/extension_helpers.c +++ b/ext/herb/extension_helpers.c @@ -20,32 +20,26 @@ const char* check_string(VALUE value) { return RSTRING_PTR(value); } -VALUE rb_position_from_c_struct(position_T* position) { - if (!position) { return Qnil; } - +VALUE rb_position_from_c_struct(position_T position) { VALUE args[2]; - args[0] = SIZET2NUM(position->line); - args[1] = SIZET2NUM(position->column); + args[0] = UINT2NUM(position.line); + args[1] = UINT2NUM(position.column); return rb_class_new_instance(2, args, cPosition); } -VALUE rb_location_from_c_struct(location_T* location) { - if (!location) { return Qnil; } - +VALUE rb_location_from_c_struct(location_T location) { VALUE args[2]; - args[0] = rb_position_from_c_struct(location->start); - args[1] = rb_position_from_c_struct(location->end); + args[0] = rb_position_from_c_struct(location.start); + args[1] = rb_position_from_c_struct(location.end); return rb_class_new_instance(2, args, cLocation); } -VALUE rb_range_from_c_struct(range_T* range) { - if (!range) { return Qnil; } - +VALUE rb_range_from_c_struct(range_T range) { VALUE args[2]; - args[0] = SIZET2NUM(range->from); - args[1] = SIZET2NUM(range->to); + args[0] = UINT2NUM(range.from); + args[1] = UINT2NUM(range.to); return rb_class_new_instance(2, args, cRange); } diff --git a/ext/herb/extension_helpers.h b/ext/herb/extension_helpers.h index 120cbf8ba..d672da2b3 100644 --- a/ext/herb/extension_helpers.h +++ b/ext/herb/extension_helpers.h @@ -12,11 +12,11 @@ const char* check_string(VALUE value); VALUE read_file_to_ruby_string(const char* file_path); -VALUE rb_position_from_c_struct(position_T* position); -VALUE rb_location_from_c_struct(location_T* location); +VALUE rb_position_from_c_struct(position_T position); +VALUE rb_location_from_c_struct(location_T location); VALUE rb_token_from_c_struct(token_T* token); -VALUE rb_range_from_c_struct(range_T* range); +VALUE rb_range_from_c_struct(range_T range); VALUE create_lex_result(array_T* tokens, VALUE source); VALUE create_parse_result(AST_DOCUMENT_NODE_T* root, VALUE source); diff --git a/javascript/packages/node/binding.gyp b/javascript/packages/node/binding.gyp index 95096ee66..8afbd6559 100644 --- a/javascript/packages/node/binding.gyp +++ b/javascript/packages/node/binding.gyp @@ -31,7 +31,6 @@ "./extension/libherb/memory.c", "./extension/libherb/parser_helpers.c", "./extension/libherb/parser.c", - "./extension/libherb/position.c", "./extension/libherb/pretty_print.c", "./extension/libherb/prism_helpers.c", "./extension/libherb/range.c", diff --git a/javascript/packages/node/extension/extension_helpers.cpp b/javascript/packages/node/extension/extension_helpers.cpp index e64c18dc1..d48b7556f 100644 --- a/javascript/packages/node/extension/extension_helpers.cpp +++ b/javascript/packages/node/extension/extension_helpers.cpp @@ -46,19 +46,13 @@ napi_value CreateString(napi_env env, const char* str) { return result; } -napi_value CreatePosition(napi_env env, position_T* position) { - if (!position) { - napi_value null_value; - napi_get_null(env, &null_value); - return null_value; - } - +napi_value CreatePosition(napi_env env, position_T position) { napi_value result; napi_create_object(env, &result); napi_value line, column; - napi_create_uint32(env, (uint32_t)position->line, &line); - napi_create_uint32(env, (uint32_t)position->column, &column); + napi_create_uint32(env, (uint32_t)position.line, &line); + napi_create_uint32(env, (uint32_t)position.column, &column); napi_set_named_property(env, result, "line", line); napi_set_named_property(env, result, "column", column); @@ -66,18 +60,12 @@ napi_value CreatePosition(napi_env env, position_T* position) { return result; } -napi_value CreateLocation(napi_env env, location_T* location) { - if (!location) { - napi_value null_value; - napi_get_null(env, &null_value); - return null_value; - } - +napi_value CreateLocation(napi_env env, location_T location) { napi_value result; napi_create_object(env, &result); - napi_value start = CreatePosition(env, location->start); - napi_value end = CreatePosition(env, location->end); + napi_value start = CreatePosition(env, location.start); + napi_value end = CreatePosition(env, location.end); napi_set_named_property(env, result, "start", start); napi_set_named_property(env, result, "end", end); @@ -85,19 +73,13 @@ napi_value CreateLocation(napi_env env, location_T* location) { return result; } -napi_value CreateRange(napi_env env, range_T* range) { - if (!range) { - napi_value null_value; - napi_get_null(env, &null_value); - return null_value; - } - +napi_value CreateRange(napi_env env, range_T range) { napi_value result; napi_create_array(env, &result); napi_value from, to; - napi_create_uint32(env, (uint32_t)range->from, &from); - napi_create_uint32(env, (uint32_t)range->to, &to); + napi_create_uint32(env, (uint32_t)range.from, &from); + napi_create_uint32(env, (uint32_t)range.to, &to); napi_set_element(env, result, 0, from); napi_set_element(env, result, 1, to); diff --git a/javascript/packages/node/extension/extension_helpers.h b/javascript/packages/node/extension/extension_helpers.h index 7387ea9d8..6bfa4f1aa 100644 --- a/javascript/packages/node/extension/extension_helpers.h +++ b/javascript/packages/node/extension/extension_helpers.h @@ -14,9 +14,9 @@ napi_value ReadFileToString(napi_env env, const char* file_path); napi_value CreateLexResult(napi_env env, array_T* tokens, napi_value source); napi_value CreateParseResult(napi_env env, AST_DOCUMENT_NODE_T* root, napi_value source); -napi_value CreateLocation(napi_env env, location_T* location); +napi_value CreateLocation(napi_env env, location_T location); napi_value CreateToken(napi_env env, token_T* token); -napi_value CreatePosition(napi_env env, position_T* position); -napi_value CreateRange(napi_env env, range_T* range); +napi_value CreatePosition(napi_env env, position_T position); +napi_value CreateRange(napi_env env, range_T range); #endif diff --git a/src/analyze.c b/src/analyze.c index 8c0a7ea4a..5ed8215db 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -158,16 +158,16 @@ static AST_NODE_T* create_control_node( control_type_t control_type ) { array_T* errors = array_init(8); - position_T* start_position = erb_node->tag_opening->location->start; - position_T* end_position = erb_node->tag_closing->location->end; + position_T start_position = erb_node->tag_opening->location.start; + position_T end_position = erb_node->tag_closing->location.end; if (end_node) { - end_position = end_node->base.location->end; + end_position = end_node->base.location.end; } else if (children && array_size(children) > 0) { AST_NODE_T* last_child = array_get(children, array_size(children) - 1); - end_position = last_child->location->end; + end_position = last_child->location.end; } else if (subsequent) { - end_position = subsequent->location->end; + end_position = subsequent->location.end; } token_T* tag_opening = erb_node->tag_opening; @@ -459,8 +459,8 @@ static size_t process_control_structure( erb_content->content, erb_content->tag_closing, when_statements, - erb_content->tag_opening->location->start, - erb_content->tag_closing->location->end, + erb_content->tag_opening->location.start, + erb_content->tag_closing->location.end, array_init(8) ); @@ -495,8 +495,8 @@ static size_t process_control_structure( erb_content->content, erb_content->tag_closing, in_statements, - erb_content->tag_opening->location->start, - erb_content->tag_closing->location->end, + erb_content->tag_opening->location.start, + erb_content->tag_closing->location.end, array_init(8) ); @@ -546,8 +546,8 @@ static size_t process_control_structure( next_erb->content, next_erb->tag_closing, else_children, - next_erb->tag_opening->location->start, - next_erb->tag_closing->location->end, + next_erb->tag_opening->location.start, + next_erb->tag_closing->location.end, array_init(8) ); } @@ -567,8 +567,8 @@ static size_t process_control_structure( end_erb->tag_opening, end_erb->content, end_erb->tag_closing, - end_erb->tag_opening->location->start, - end_erb->tag_closing->location->end, + end_erb->tag_opening->location.start, + end_erb->tag_closing->location.end, end_erb->base.errors ); @@ -577,19 +577,19 @@ static size_t process_control_structure( } } - position_T* start_position = erb_node->tag_opening->location->start; - position_T* end_position = erb_node->tag_closing->location->end; + position_T start_position = erb_node->tag_opening->location.start; + position_T end_position = erb_node->tag_closing->location.end; if (end_node) { - end_position = end_node->base.location->end; + end_position = end_node->base.location.end; } else if (else_clause) { - end_position = else_clause->base.location->end; + end_position = else_clause->base.location.end; } else if (array_size(when_conditions) > 0) { AST_NODE_T* last_when = array_get(when_conditions, array_size(when_conditions) - 1); - end_position = last_when->location->end; + end_position = last_when->location.end; } else if (array_size(in_conditions) > 0) { AST_NODE_T* last_in = array_get(in_conditions, array_size(in_conditions) - 1); - end_position = last_in->location->end; + end_position = last_in->location.end; } if (array_size(in_conditions) > 0) { @@ -682,8 +682,8 @@ static size_t process_control_structure( next_erb->content, next_erb->tag_closing, else_children, - next_erb->tag_opening->location->start, - next_erb->tag_closing->location->end, + next_erb->tag_opening->location.start, + next_erb->tag_closing->location.end, array_init(8) ); } @@ -723,8 +723,8 @@ static size_t process_control_structure( next_erb->content, next_erb->tag_closing, ensure_children, - next_erb->tag_opening->location->start, - next_erb->tag_closing->location->end, + next_erb->tag_opening->location.start, + next_erb->tag_closing->location.end, array_init(8) ); } @@ -744,8 +744,8 @@ static size_t process_control_structure( end_erb->tag_opening, end_erb->content, end_erb->tag_closing, - end_erb->tag_opening->location->start, - end_erb->tag_closing->location->end, + end_erb->tag_opening->location.start, + end_erb->tag_closing->location.end, end_erb->base.errors ); @@ -754,17 +754,17 @@ static size_t process_control_structure( } } - position_T* start_position = erb_node->tag_opening->location->start; - position_T* end_position = erb_node->tag_closing->location->end; + position_T start_position = erb_node->tag_opening->location.start; + position_T end_position = erb_node->tag_closing->location.end; if (end_node) { - end_position = end_node->base.location->end; + end_position = end_node->base.location.end; } else if (ensure_clause) { - end_position = ensure_clause->base.location->end; + end_position = ensure_clause->base.location.end; } else if (else_clause) { - end_position = else_clause->base.location->end; + end_position = else_clause->base.location.end; } else if (rescue_clause) { - end_position = rescue_clause->base.location->end; + end_position = rescue_clause->base.location.end; } AST_ERB_BEGIN_NODE_T* begin_node = ast_erb_begin_node_init( @@ -802,8 +802,8 @@ static size_t process_control_structure( close_erb->tag_opening, close_erb->content, close_erb->tag_closing, - close_erb->tag_opening->location->start, - close_erb->tag_closing->location->end, + close_erb->tag_opening->location.start, + close_erb->tag_closing->location.end, close_erb->base.errors ); @@ -812,14 +812,14 @@ static size_t process_control_structure( } } - position_T* start_position = erb_node->tag_opening->location->start; - position_T* end_position = erb_node->tag_closing->location->end; + position_T start_position = erb_node->tag_opening->location.start; + position_T end_position = erb_node->tag_closing->location.end; if (end_node) { - end_position = end_node->base.location->end; + end_position = end_node->base.location.end; } else if (children && array_size(children) > 0) { AST_NODE_T* last_child = array_get(children, array_size(children) - 1); - end_position = last_child->location->end; + end_position = last_child->location.end; } AST_ERB_BLOCK_NODE_T* block_node = ast_erb_block_node_init( @@ -866,8 +866,8 @@ static size_t process_control_structure( end_erb->tag_opening, end_erb->content, end_erb->tag_closing, - end_erb->tag_opening->location->start, - end_erb->tag_closing->location->end, + end_erb->tag_opening->location.start, + end_erb->tag_closing->location.end, end_erb->base.errors ); diff --git a/src/ast_node.c b/src/ast_node.c index 350599c13..c70acb936 100644 --- a/src/ast_node.c +++ b/src/ast_node.c @@ -12,11 +12,12 @@ size_t ast_node_sizeof(void) { return sizeof(struct AST_NODE_STRUCT); } -void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T* start, position_T* end, array_T* errors) { +void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T start, position_T end, array_T* errors) { if (!node) { return; } node->type = type; - node->location = location_init(position_copy(start), position_copy(end)); + node->location.start = start; + node->location.end = end; if (errors == NULL) { node->errors = array_init(8); @@ -28,7 +29,7 @@ void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T* sta AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token) { AST_LITERAL_NODE_T* literal = malloc(sizeof(AST_LITERAL_NODE_T)); - ast_node_init(&literal->base, AST_LITERAL_NODE, token->location->start, token->location->end, NULL); + ast_node_init(&literal->base, AST_LITERAL_NODE, token->location.start, token->location.end, NULL); literal->content = herb_strdup(token->value); @@ -51,24 +52,20 @@ void ast_node_append_error(const AST_NODE_T* node, ERROR_T* error) { array_append(node->errors, error); } -void ast_node_set_start(AST_NODE_T* node, position_T* position) { - if (node->location->start != NULL) { position_free(node->location->start); } - - node->location->start = position_copy(position); +void ast_node_set_start(AST_NODE_T* node, position_T position) { + node->location.start = position; } -void ast_node_set_end(AST_NODE_T* node, position_T* position) { - if (node->location->end != NULL) { position_free(node->location->end); } - - node->location->end = position_copy(position); +void ast_node_set_end(AST_NODE_T* node, position_T position) { + node->location.end = position; } void ast_node_set_start_from_token(AST_NODE_T* node, const token_T* token) { - ast_node_set_start(node, token->location->start); + ast_node_set_start(node, token->location.start); } void ast_node_set_end_from_token(AST_NODE_T* node, const token_T* token) { - ast_node_set_end(node, token->location->end); + ast_node_set_end(node, token->location.end); } void ast_node_set_positions_from_token(AST_NODE_T* node, const token_T* token) { diff --git a/src/include/ast_node.h b/src/include/ast_node.h index e41ece34a..23af16d0c 100644 --- a/src/include/ast_node.h +++ b/src/include/ast_node.h @@ -6,7 +6,7 @@ #include "position.h" #include "token_struct.h" -void ast_node_init(AST_NODE_T* node, ast_node_type_T type, position_T* start, position_T* end, array_T* errors); +void ast_node_init(AST_NODE_T* node, ast_node_type_T type, position_T start, position_T end, array_T* errors); void ast_node_free(AST_NODE_T* node); AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token); @@ -18,8 +18,8 @@ ast_node_type_T ast_node_type(const AST_NODE_T* node); char* ast_node_name(AST_NODE_T* node); -void ast_node_set_start(AST_NODE_T* node, position_T* position); -void ast_node_set_end(AST_NODE_T* node, position_T* position); +void ast_node_set_start(AST_NODE_T* node, position_T position); +void ast_node_set_end(AST_NODE_T* node, position_T position); size_t ast_node_errors_count(const AST_NODE_T* node); array_T* ast_node_errors(const AST_NODE_T* node); diff --git a/src/include/lexer_peek_helpers.h b/src/include/lexer_peek_helpers.h index faa0ecec8..c89ca2e08 100644 --- a/src/include/lexer_peek_helpers.h +++ b/src/include/lexer_peek_helpers.h @@ -5,16 +5,17 @@ #include "token_struct.h" #include +#include #include #include typedef struct { - size_t position; - size_t line; - size_t column; - size_t previous_position; - size_t previous_line; - size_t previous_column; + uint32_t position; + uint32_t line; + uint32_t column; + uint32_t previous_position; + uint32_t previous_line; + uint32_t previous_column; char current_character; lexer_state_T state; } lexer_state_snapshot_T; diff --git a/src/include/lexer_struct.h b/src/include/lexer_struct.h index 709233884..0b305acf5 100644 --- a/src/include/lexer_struct.h +++ b/src/include/lexer_struct.h @@ -2,6 +2,7 @@ #define HERB_LEXER_STRUCT_H #include +#include #include typedef enum { @@ -12,20 +13,20 @@ typedef enum { typedef struct LEXER_STRUCT { const char* source; - size_t source_length; + uint32_t source_length; - size_t current_line; - size_t current_column; - size_t current_position; + uint32_t current_line; + uint32_t current_column; + uint32_t current_position; - size_t previous_line; - size_t previous_column; - size_t previous_position; + uint32_t previous_line; + uint32_t previous_column; + uint32_t previous_position; char current_character; lexer_state_T state; - size_t stall_counter; - size_t last_position; + uint32_t stall_counter; + uint32_t last_position; bool stalled; } lexer_T; diff --git a/src/include/location.h b/src/include/location.h index ef07ba688..d01c94aa9 100644 --- a/src/include/location.h +++ b/src/include/location.h @@ -1,25 +1,22 @@ #ifndef HERB_LOCATION_H #define HERB_LOCATION_H +#include #include #include "position.h" typedef struct LOCATION_STRUCT { - position_T* start; - position_T* end; + position_T start; + position_T end; } location_T; -location_T* location_init(position_T* start, position_T* end); -location_T* location_from(size_t start_line, size_t start_column, size_t end_line, size_t end_column); - -position_T* location_start(location_T* location); -position_T* location_end_(location_T* location); - -size_t location_sizeof(void); - -location_T* location_copy(location_T* location); - -void location_free(location_T* location); +void location_from( + location_T* location, + uint32_t start_line, + uint32_t start_column, + uint32_t end_line, + uint32_t end_column +); #endif diff --git a/src/include/parser_helpers.h b/src/include/parser_helpers.h index aff4ac8ae..eba20f447 100644 --- a/src/include/parser_helpers.h +++ b/src/include/parser_helpers.h @@ -19,7 +19,7 @@ void parser_append_literal_node_from_buffer( const parser_T* parser, buffer_T* buffer, array_T* children, - position_T* start + position_T start ); bool parser_in_svg_context(const parser_T* parser); diff --git a/src/include/position.h b/src/include/position.h index 95e64044b..8fad03d5f 100644 --- a/src/include/position.h +++ b/src/include/position.h @@ -1,22 +1,11 @@ #ifndef HERB_POSITION_H #define HERB_POSITION_H -#include +#include typedef struct POSITION_STRUCT { - size_t line; - size_t column; + uint32_t line; + uint32_t column; } position_T; -position_T* position_init(size_t line, size_t column); - -size_t position_line(const position_T* position); -size_t position_column(const position_T* position); - -size_t position_sizeof(void); - -position_T* position_copy(position_T* position); - -void position_free(position_T* position); - #endif diff --git a/src/include/pretty_print.h b/src/include/pretty_print.h index 610bc3f73..7cabef7af 100644 --- a/src/include/pretty_print.h +++ b/src/include/pretty_print.h @@ -21,7 +21,7 @@ void pretty_print_position_property( buffer_T* buffer ); -void pretty_print_location(location_T* location, buffer_T* buffer); +void pretty_print_location(location_T location, buffer_T* buffer); void pretty_print_property( const char* name, diff --git a/src/include/prism_helpers.h b/src/include/prism_helpers.h index 66438c342..ebfb53b4f 100644 --- a/src/include/prism_helpers.h +++ b/src/include/prism_helpers.h @@ -16,6 +16,6 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error( pm_parser_t* parser ); -position_T* position_from_source_with_offset(const char* source, size_t offset); +position_T position_from_source_with_offset(const char* source, size_t offset); #endif diff --git a/src/include/range.h b/src/include/range.h index c5c28f8bb..019b1962b 100644 --- a/src/include/range.h +++ b/src/include/range.h @@ -1,23 +1,14 @@ #ifndef HERB_RANGE_H #define HERB_RANGE_H +#include #include typedef struct RANGE_STRUCT { - size_t from; - size_t to; + uint32_t from; + uint32_t to; } range_T; -range_T* range_init(size_t from, size_t to); - -size_t range_from(const range_T* range); -size_t range_to(const range_T* range); -size_t range_length(range_T* range); - -range_T* range_copy(range_T* range); - -size_t range_sizeof(void); - -void range_free(range_T* range); +uint32_t range_length(range_T range); #endif diff --git a/src/include/token.h b/src/include/token.h index 6930439e9..523dd1f04 100644 --- a/src/include/token.h +++ b/src/include/token.h @@ -13,9 +13,6 @@ const char* token_type_to_string(token_type_T type); char* token_value(const token_T* token); int token_type(const token_T* token); -position_T* token_start_position(token_T* token); -position_T* token_end_position(token_T* token); - size_t token_sizeof(void); token_T* token_copy(token_T* token); diff --git a/src/include/token_struct.h b/src/include/token_struct.h index e5192165b..2727d2a4c 100644 --- a/src/include/token_struct.h +++ b/src/include/token_struct.h @@ -50,8 +50,8 @@ typedef enum { typedef struct TOKEN_STRUCT { char* value; - range_T* range; - location_T* location; + range_T range; + location_T location; token_type_T type; } token_T; diff --git a/src/lexer.c b/src/lexer.c index 513f67882..f62bbd696 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -42,7 +42,7 @@ lexer_T* lexer_init(const char* source) { lexer->state = STATE_DATA; lexer->source = source; - lexer->source_length = strlen(source); + lexer->source_length = (uint32_t) strlen(source); lexer->current_character = source[0]; lexer->current_line = 1; @@ -66,7 +66,7 @@ token_T* lexer_error(lexer_T* lexer, const char* message) { snprintf( error_message, sizeof(error_message), - "[Lexer] Error: %s (character '%c', line %zu, col %zu)\n", + "[Lexer] Error: %s (character '%c', line %u, col %u)\n", message, lexer->current_character, lexer->current_line, diff --git a/src/lexer_peek_helpers.c b/src/lexer_peek_helpers.c index a46bbeff6..a1f96fa5e 100644 --- a/src/lexer_peek_helpers.c +++ b/src/lexer_peek_helpers.c @@ -77,9 +77,9 @@ bool lexer_peek_erb_end(const lexer_T* lexer, const int offset) { } bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T token_type) { - size_t saved_position = lexer->current_position; - size_t saved_line = lexer->current_line; - size_t saved_column = lexer->current_column; + uint32_t saved_position = lexer->current_position; + uint32_t saved_line = lexer->current_line; + uint32_t saved_column = lexer->current_column; char saved_character = lexer->current_character; lexer_state_T saved_state = lexer->state; diff --git a/src/location.c b/src/location.c index 1c83c5e49..aec168d3c 100644 --- a/src/location.c +++ b/src/location.c @@ -1,41 +1,13 @@ #include "include/location.h" -#include "include/memory.h" #include "include/position.h" -size_t location_sizeof(void) { - return sizeof(location_T); -} - -location_T* location_init(position_T* start, position_T* end) { - location_T* location = safe_malloc(location_sizeof()); - - location->start = start; - location->end = end; - - return location; -} - -location_T* location_from(size_t start_line, size_t start_column, size_t end_line, size_t end_column) { - return location_init(position_init(start_line, start_column), position_init(end_line, end_column)); -} - -position_T* location_start(location_T* location) { - return location->start; -} - -position_T* location_end(location_T* location) { - return location->end; -} - -location_T* location_copy(location_T* location) { - if (location == NULL) { return NULL; } - - return location_init(position_copy(location->start), position_copy(location->end)); -} - -void location_free(location_T* location) { - if (location->start != NULL) { position_free(location->start); } - if (location->end != NULL) { position_free(location->end); } - - free(location); +void location_from( + location_T* location, + uint32_t start_line, + uint32_t start_column, + uint32_t end_line, + uint32_t end_column +) { + location->start = (position_T) { .line = start_line, .column = start_column }; + location->end = (position_T) { .line = end_line, .column = end_column }; } diff --git a/src/parser.c b/src/parser.c index 3e4e4ffde..47847d9b5 100644 --- a/src/parser.c +++ b/src/parser.c @@ -56,15 +56,14 @@ static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) { buffer_T content = buffer_new(); token_T* tag_opening = parser_consume_expected(parser, TOKEN_CDATA_START, errors); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; while (token_is_none_of(parser, TOKEN_CDATA_END, TOKEN_EOF)) { if (token_is(parser, TOKEN_ERB_START)) { parser_append_literal_node_from_buffer(parser, &content, children, start); AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser); array_append(children, erb_node); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -80,12 +79,11 @@ static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) { tag_opening, children, tag_closing, - tag_opening->location->start, - tag_closing->location->end, + tag_opening->location.start, + tag_closing->location.end, errors ); - position_free(start); buffer_free(&content); token_free(tag_opening); token_free(tag_closing); @@ -97,7 +95,7 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { array_T* errors = array_init(8); array_T* children = array_init(8); token_T* comment_start = parser_consume_expected(parser, TOKEN_HTML_COMMENT_START, errors); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; buffer_T comment = buffer_new(); @@ -108,8 +106,7 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser); array_append(children, erb_node); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -127,13 +124,12 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { comment_start, children, comment_end, - comment_start->location->start, - comment_end->location->end, + comment_start->location.start, + comment_end->location.end, errors ); buffer_free(&comment); - position_free(start); token_free(comment_start); token_free(comment_end); @@ -147,7 +143,7 @@ static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) { token_T* tag_opening = parser_consume_expected(parser, TOKEN_HTML_DOCTYPE, errors); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; while (token_is_none_of(parser, TOKEN_HTML_TAG_END, TOKEN_EOF)) { if (token_is(parser, TOKEN_ERB_START)) { @@ -172,12 +168,11 @@ static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) { tag_opening, children, tag_closing, - tag_opening->location->start, - tag_closing->location->end, + tag_opening->location.start, + tag_closing->location.end, errors ); - position_free(start); token_free(tag_opening); token_free(tag_closing); buffer_free(&content); @@ -192,7 +187,7 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser token_T* tag_opening = parser_consume_expected(parser, TOKEN_XML_DECLARATION, errors); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; while (token_is_none_of(parser, TOKEN_XML_DECLARATION_END, TOKEN_EOF)) { if (token_is(parser, TOKEN_ERB_START)) { @@ -201,8 +196,7 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser); array_append(children, erb_node); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -220,12 +214,11 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser tag_opening, children, tag_closing, - tag_opening->location->start, - tag_closing->location->end, + tag_opening->location.start, + tag_closing->location.end, errors ); - position_free(start); token_free(tag_opening); token_free(tag_closing); buffer_free(&content); @@ -234,7 +227,7 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser } static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T* document_errors) { - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; buffer_T content = buffer_new(); @@ -255,13 +248,12 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T "Token Error", "not TOKEN_ERROR", token->value, - token->location->start, - token->location->end, + token->location.start, + token->location.end, document_errors ); token_free(token); - position_free(start); return NULL; } @@ -275,17 +267,15 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T if (buffer_length(&content) > 0) { AST_HTML_TEXT_NODE_T* text_node = - ast_html_text_node_init(buffer_value(&content), start, parser->current_token->location->start, errors); + ast_html_text_node_init(buffer_value(&content), start, parser->current_token->location.start, errors); - position_free(start); buffer_free(&content); return text_node; } - AST_HTML_TEXT_NODE_T* text_node = ast_html_text_node_init("", start, parser->current_token->location->start, errors); + AST_HTML_TEXT_NODE_T* text_node = ast_html_text_node_init("", start, parser->current_token->location.start, errors); - position_free(start); buffer_free(&content); return text_node; @@ -295,7 +285,7 @@ static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T array_T* errors = array_init(8); array_T* children = array_init(8); buffer_T buffer = buffer_new(); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; while (token_is_none_of( parser, @@ -312,8 +302,7 @@ static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser); array_append(children, erb_node); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -324,26 +313,23 @@ static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T parser_append_literal_node_from_buffer(parser, &buffer, children, start); - position_T* node_start = NULL; - position_T* node_end = NULL; + position_T node_start = { 0 }; + position_T node_end = { 0 }; if (children->size > 0) { AST_NODE_T* first_child = array_get(children, 0); AST_NODE_T* last_child = array_get(children, children->size - 1); - node_start = position_copy(first_child->location->start); - node_end = position_copy(last_child->location->end); + node_start = first_child->location.start; + node_end = last_child->location.end; } else { - node_start = position_copy(parser->current_token->location->start); - node_end = position_copy(parser->current_token->location->start); + node_start = parser->current_token->location.start; + node_end = parser->current_token->location.start; } AST_HTML_ATTRIBUTE_NAME_NODE_T* attribute_name = ast_html_attribute_name_node_init(children, node_start, node_end, errors); - position_free(start); - position_free(node_start); - position_free(node_end); buffer_free(&buffer); return attribute_name; @@ -356,7 +342,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value ) { buffer_T buffer = buffer_new(); token_T* opening_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; while (!token_is(parser, TOKEN_EOF) && !( @@ -368,8 +354,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value array_append(children, parser_parse_erb_tag(parser)); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -414,8 +399,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value "Unescaped quote character in attribute value", "escaped quote (\\') or different quote style (\")", opening_quote->value, - potential_closing->location->start, - potential_closing->location->end, + potential_closing->location.start, + potential_closing->location.end, errors ); @@ -438,8 +423,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value array_append(children, parser_parse_erb_tag(parser)); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -458,7 +442,6 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value } parser_append_literal_node_from_buffer(parser, &buffer, children, start); - position_free(start); buffer_free(&buffer); token_T* closing_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors); @@ -467,8 +450,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value append_quotes_mismatch_error( opening_quote, closing_quote, - closing_quote->location->start, - closing_quote->location->end, + closing_quote->location.start, + closing_quote->location.end, errors ); } @@ -478,8 +461,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value children, closing_quote, true, - opening_quote->location->start, - closing_quote->location->end, + opening_quote->location.start, + closing_quote->location.end, errors ); @@ -503,8 +486,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser children, NULL, false, - erb_node->base.location->start, - erb_node->base.location->end, + erb_node->base.location.start, + erb_node->base.location.end, errors ); } @@ -522,8 +505,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser children, NULL, false, - literal->base.location->start, - literal->base.location->end, + literal->base.location.start, + literal->base.location.end, errors ); } @@ -533,8 +516,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser if (token_is(parser, TOKEN_BACKTICK)) { token_T* token = parser_advance(parser); - position_T* start = position_copy(token->location->start); - position_T* end = position_copy(token->location->end); + position_T start = token->location.start; + position_T end = token->location.end; append_unexpected_error( "Invalid quote character for HTML attribute", @@ -548,8 +531,6 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser AST_HTML_ATTRIBUTE_VALUE_NODE_T* value = ast_html_attribute_value_node_init(NULL, children, NULL, false, start, end, errors); - position_free(start); - position_free(end); token_free(token); return value; @@ -559,8 +540,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser "Unexpected Token", "TOKEN_IDENTIFIER, TOKEN_QUOTE, TOKEN_ERB_START", token_type_to_string(parser->current_token->type), - parser->current_token->location->start, - parser->current_token->location->end, + parser->current_token->location.start, + parser->current_token->location.end, errors ); @@ -569,8 +550,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser children, NULL, false, - parser->current_token->location->start, - parser->current_token->location->end, + parser->current_token->location.start, + parser->current_token->location.end, errors ); @@ -586,17 +567,19 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) if (has_equals) { buffer_T equals_buffer = buffer_new(); - position_T* equals_start = NULL; - position_T* equals_end = NULL; - size_t range_start = 0; - size_t range_end = 0; + position_T equals_start = { 0 }; + position_T equals_end = { 0 }; + uint32_t range_start = 0; + uint32_t range_end = 0; + bool equals_start_present = false; while (token_is_any_of(parser, TOKEN_WHITESPACE, TOKEN_NEWLINE)) { token_T* whitespace = parser_advance(parser); - if (equals_start == NULL) { - equals_start = position_copy(whitespace->location->start); - range_start = whitespace->range->from; + if (equals_start_present == false) { + equals_start_present = true; + equals_start = whitespace->location.start; + range_start = whitespace->range.from; } buffer_append(&equals_buffer, whitespace->value); @@ -605,29 +588,30 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) token_T* equals = parser_advance(parser); - if (equals_start == NULL) { - equals_start = position_copy(equals->location->start); - range_start = equals->range->from; + if (equals_start_present == false) { + equals_start_present = true; + equals_start = equals->location.start; + range_start = equals->range.from; } buffer_append(&equals_buffer, equals->value); - equals_end = position_copy(equals->location->end); - range_end = equals->range->to; + equals_end = equals->location.end; + range_end = equals->range.to; token_free(equals); while (token_is_any_of(parser, TOKEN_WHITESPACE, TOKEN_NEWLINE)) { token_T* whitespace = parser_advance(parser); buffer_append(&equals_buffer, whitespace->value); - equals_end = position_copy(whitespace->location->end); - range_end = whitespace->range->to; + equals_end = whitespace->location.end; + range_end = whitespace->range.to; token_free(whitespace); } token_T* equals_with_whitespace = calloc(1, sizeof(token_T)); equals_with_whitespace->type = TOKEN_EQUALS; equals_with_whitespace->value = herb_strdup(equals_buffer.value); - equals_with_whitespace->location = location_init(equals_start, equals_end); - equals_with_whitespace->range = range_init(range_start, range_end); + equals_with_whitespace->location = (location_T) { .start = equals_start, .end = equals_end }; + equals_with_whitespace->range = (range_T) { .from = range_start, .to = range_end }; buffer_free(&equals_buffer); @@ -637,8 +621,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) attribute_name, equals_with_whitespace, attribute_value, - attribute_name->base.location->start, - attribute_value->base.location->end, + attribute_name->base.location.start, + attribute_value->base.location.end, NULL ); } else { @@ -646,8 +630,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) attribute_name, NULL, NULL, - attribute_name->base.location->start, - attribute_name->base.location->end, + attribute_name->base.location.start, + attribute_name->base.location.end, NULL ); } @@ -666,8 +650,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) attribute_name, equals, attribute_value, - attribute_name->base.location->start, - attribute_value->base.location->end, + attribute_name->base.location.start, + attribute_value->base.location.end, NULL ); @@ -680,8 +664,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) attribute_name, NULL, NULL, - attribute_name->base.location->start, - attribute_name->base.location->end, + attribute_name->base.location.start, + attribute_name->base.location.end, NULL ); } @@ -849,8 +833,8 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) { tag_end, children, is_self_closing, - tag_start->location->start, - tag_end->location->end, + tag_start->location.start, + tag_end->location.end, errors ); @@ -883,8 +867,8 @@ static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser) tag_name, expected, got, - tag_opening->location->start, - tag_closing->location->end, + tag_opening->location.start, + tag_closing->location.end, errors ); @@ -897,8 +881,8 @@ static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser) tag_name, children, tag_closing, - tag_opening->location->start, - tag_closing->location->end, + tag_opening->location.start, + tag_closing->location.end, errors ); @@ -921,8 +905,8 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_self_closing_element( NULL, true, ELEMENT_SOURCE_HTML, - open_tag->base.location->start, - open_tag->base.location->end, + open_tag->base.location.start, + open_tag->base.location.end, NULL ); } @@ -970,8 +954,8 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_regular_element( close_tag, false, ELEMENT_SOURCE_HTML, - open_tag->base.location->start, - close_tag->base.location->end, + open_tag->base.location.start, + close_tag->base.location.end, errors ); } @@ -1001,8 +985,8 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_element(parser_T* parser) { NULL, false, ELEMENT_SOURCE_HTML, - open_tag->base.location->start, - open_tag->base.location->end, + open_tag->base.location.start, + open_tag->base.location.end, errors ); } @@ -1021,8 +1005,8 @@ static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) { NULL, false, false, - opening_tag->location->start, - closing_tag->location->end, + opening_tag->location.start, + closing_tag->location.end, errors ); @@ -1035,12 +1019,11 @@ static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) { static void parser_parse_foreign_content(parser_T* parser, array_T* children, array_T* errors) { buffer_T content = buffer_new(); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; const char* expected_closing_tag = parser_get_foreign_content_closing_tag(parser->foreign_content_type); if (expected_closing_tag == NULL) { parser_exit_foreign_content(parser); - position_free(start); buffer_free(&content); return; @@ -1053,8 +1036,7 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser); array_append(children, erb_node); - position_free(start); - start = position_copy(parser->current_token->location->start); + start = parser->current_token->location.start; continue; } @@ -1077,7 +1059,6 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar parser_append_literal_node_from_buffer(parser, &content, children, start); parser_exit_foreign_content(parser); - position_free(start); buffer_free(&content); return; @@ -1091,7 +1072,6 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar parser_append_literal_node_from_buffer(parser, &content, children, start); parser_exit_foreign_content(parser); - position_free(start); buffer_free(&content); } @@ -1167,8 +1147,8 @@ static void parser_parse_unclosed_html_tags(const parser_T* parser, array_T* err append_unclosed_element_error( unclosed_tag, - parser->current_token->location->start, - parser->current_token->location->end, + parser->current_token->location.start, + parser->current_token->location.end, errors ); @@ -1192,8 +1172,8 @@ static void parser_parse_stray_closing_tags(parser_T* parser, array_T* children, if (!is_void_element(close_tag->tag_name->value)) { append_missing_opening_tag_error( close_tag->tag_name, - close_tag->base.location->start, - close_tag->base.location->end, + close_tag->base.location.start, + close_tag->base.location.end, close_tag->base.errors ); } @@ -1207,7 +1187,7 @@ static void parser_parse_stray_closing_tags(parser_T* parser, array_T* children, static AST_DOCUMENT_NODE_T* parser_parse_document(parser_T* parser) { array_T* children = array_init(8); array_T* errors = array_init(8); - position_T* start = position_copy(parser->current_token->location->start); + position_T start = parser->current_token->location.start; parser_parse_in_data_state(parser, children, errors); parser_parse_unclosed_html_tags(parser, errors); @@ -1215,9 +1195,8 @@ static AST_DOCUMENT_NODE_T* parser_parse_document(parser_T* parser) { token_T* eof = parser_consume_expected(parser, TOKEN_EOF, errors); - AST_DOCUMENT_NODE_T* document_node = ast_document_node_init(children, start, eof->location->end, errors); + AST_DOCUMENT_NODE_T* document_node = ast_document_node_init(children, start, eof->location.end, errors); - position_free(start); token_free(eof); return document_node; @@ -1232,8 +1211,8 @@ static void parser_handle_whitespace(parser_T* parser, token_T* whitespace_token array_T* errors = array_init(8); AST_WHITESPACE_NODE_T* whitespace_node = ast_whitespace_node_init( whitespace_token, - whitespace_token->location->start, - whitespace_token->location->end, + whitespace_token->location.start, + whitespace_token->location.end, errors ); array_append(children, whitespace_node); diff --git a/src/parser_helpers.c b/src/parser_helpers.c index bf92a5523..0c848f9ec 100644 --- a/src/parser_helpers.c +++ b/src/parser_helpers.c @@ -99,8 +99,8 @@ void parser_append_unexpected_error(parser_T* parser, const char* description, c description, expected, token_type_to_string(token->type), - token->location->start, - token->location->end, + token->location.start, + token->location.end, errors ); @@ -111,8 +111,8 @@ void parser_append_unexpected_token_error(parser_T* parser, token_type_T expecte append_unexpected_token_error( expected_type, parser->current_token, - parser->current_token->location->start, - parser->current_token->location->end, + parser->current_token->location.start, + parser->current_token->location.end, errors ); } @@ -121,12 +121,12 @@ void parser_append_literal_node_from_buffer( const parser_T* parser, buffer_T* buffer, array_T* children, - position_T* start + position_T start ) { if (buffer_length(buffer) == 0) { return; } AST_LITERAL_NODE_T* literal = - ast_literal_node_init(buffer_value(buffer), start, parser->current_token->location->start, NULL); + ast_literal_node_init(buffer_value(buffer), start, parser->current_token->location.start, NULL); if (children != NULL) { array_append(children, literal); } buffer_clear(buffer); @@ -149,7 +149,7 @@ token_T* parser_consume_expected(parser_T* parser, const token_type_T expected_t if (token == NULL) { token = parser_advance(parser); - append_unexpected_token_error(expected_type, token, token->location->start, token->location->end, array); + append_unexpected_token_error(expected_type, token, token->location.start, token->location.end, array); } return token; @@ -162,8 +162,8 @@ AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag( ) { append_missing_closing_tag_error( open_tag->tag_name, - open_tag->tag_name->location->start, - open_tag->tag_name->location->end, + open_tag->tag_name->location.start, + open_tag->tag_name->location.end, errors ); @@ -174,8 +174,8 @@ AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag( NULL, false, ELEMENT_SOURCE_HTML, - open_tag->base.location->start, - open_tag->base.location->end, + open_tag->base.location.start, + open_tag->base.location.end, errors ); } @@ -192,15 +192,15 @@ void parser_handle_mismatched_tags( append_tag_names_mismatch_error( expected_tag, actual_tag, - actual_tag->location->start, - actual_tag->location->end, + actual_tag->location.start, + actual_tag->location.end, errors ); } else { append_missing_opening_tag_error( close_tag->tag_name, - close_tag->tag_name->location->start, - close_tag->tag_name->location->end, + close_tag->tag_name->location.start, + close_tag->tag_name->location.end, errors ); } diff --git a/src/position.c b/src/position.c deleted file mode 100644 index 7b946b764..000000000 --- a/src/position.c +++ /dev/null @@ -1,33 +0,0 @@ -#include "include/position.h" -#include "include/memory.h" - -size_t position_sizeof(void) { - return sizeof(position_T); -} - -position_T* position_init(const size_t line, const size_t column) { - position_T* position = safe_malloc(position_sizeof()); - - position->line = line; - position->column = column; - - return position; -} - -size_t position_line(const position_T* position) { - return position->line; -} - -size_t position_column(const position_T* position) { - return position->column; -} - -position_T* position_copy(position_T* position) { - if (position == NULL) { return NULL; } - - return position_init(position_line(position), position_column(position)); -} - -void position_free(position_T* position) { - free(position); -} diff --git a/src/pretty_print.c b/src/pretty_print.c index ee79d4e34..85b2f5894 100644 --- a/src/pretty_print.c +++ b/src/pretty_print.c @@ -158,16 +158,16 @@ void pretty_print_errors( } } -void pretty_print_location(location_T* location, buffer_T* buffer) { +void pretty_print_location(location_T location, buffer_T* buffer) { buffer_append(buffer, "(location: ("); char location_string[128]; sprintf( location_string, - "%zu,%zu)-(%zu,%zu", - (location->start && location->start->line) ? location->start->line : 0, - (location->start && location->start->column) ? location->start->column : 0, - (location->end && location->end->line) ? location->end->line : 0, - (location->end && location->end->column) ? location->end->column : 0 + "%u,%u)-(%u,%u", + location.start.line, + location.start.column, + location.end.line, + location.end.column ); buffer_append(buffer, location_string); buffer_append(buffer, "))"); @@ -188,12 +188,7 @@ void pretty_print_position_property( char position_string[128]; - sprintf( - position_string, - "%zu:%zu", - (position->line) ? position->line : 0, - (position->column) ? position->column : 0 - ); + sprintf(position_string, "%u:%u", (position->line) ? position->line : 0, (position->column) ? position->column : 0); buffer_append(buffer, position_string); buffer_append(buffer, ")"); diff --git a/src/prism_helpers.c b/src/prism_helpers.c index 93c4857f2..06ac155b5 100644 --- a/src/prism_helpers.c +++ b/src/prism_helpers.c @@ -16,15 +16,15 @@ const char* pm_error_level_to_string(pm_error_level_t level) { } } -position_T* position_from_source_with_offset(const char* source, size_t offset) { - position_T* position = position_init(1, 0); +position_T position_from_source_with_offset(const char* source, size_t offset) { + position_T position = { .line = 1, .column = 0 }; for (size_t i = 0; i < offset; i++) { if (is_newline(source[i])) { - position->line++; - position->column = 0; + position.line++; + position.column = 0; } else { - position->column++; + position.column++; } } @@ -40,8 +40,8 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error( size_t start_offset = (size_t) (error->location.start - parser->start); size_t end_offset = (size_t) (error->location.end - parser->start); - position_T* start = position_from_source_with_offset(source, start_offset); - position_T* end = position_from_source_with_offset(source, end_offset); + position_T start = position_from_source_with_offset(source, start_offset); + position_T end = position_from_source_with_offset(source, end_offset); return ruby_parse_error_init( error->message, diff --git a/src/range.c b/src/range.c index 030b87b71..6e6abea50 100644 --- a/src/range.c +++ b/src/range.c @@ -1,38 +1,5 @@ #include "include/range.h" -size_t range_sizeof(void) { - return sizeof(range_T); -} - -range_T* range_init(const size_t from, const size_t to) { - range_T* range = calloc(1, range_sizeof()); - - range->from = from; - range->to = to; - - return range; -} - -size_t range_from(const range_T* range) { - return range->from; -} - -size_t range_to(const range_T* range) { - return range->to; -} - -size_t range_length(range_T* range) { - return range_to(range) - range_from(range); -} - -range_T* range_copy(range_T* range) { - if (!range) { return NULL; } - - return range_init(range_from(range), range_to(range)); -} - -void range_free(range_T* range) { - if (range == NULL) { return; } - - free(range); +uint32_t range_length(range_T range) { + return range.to - range.from; } diff --git a/src/token.c b/src/token.c index a8cb5fb3f..37c7d4d89 100644 --- a/src/token.c +++ b/src/token.c @@ -2,6 +2,7 @@ #include "include/json.h" #include "include/lexer.h" #include "include/position.h" +#include "include/range.h" #include "include/token_struct.h" #include "include/util.h" @@ -28,10 +29,15 @@ token_T* token_init(const char* value, const token_type_T type, lexer_T* lexer) } token->type = type; - token->range = range_init(lexer->previous_position, lexer->current_position); - - token->location = - location_from(lexer->previous_line, lexer->previous_column, lexer->current_line, lexer->current_column); + token->range = (range_T) { .from = lexer->previous_position, .to = lexer->current_position }; + + location_from( + &token->location, + lexer->previous_line, + lexer->previous_column, + lexer->current_line, + lexer->current_column + ); lexer->previous_line = lexer->current_line; lexer->previous_column = lexer->current_column; @@ -84,7 +90,7 @@ const char* token_type_to_string(const token_type_T type) { char* token_to_string(const token_T* token) { const char* type_string = token_type_to_string(token->type); - const char* template = "#"; + const char* template = "#"; char* string = calloc(strlen(type_string) + strlen(template) + strlen(token->value) + 16, sizeof(char)); char* escaped; @@ -100,12 +106,12 @@ char* token_to_string(const token_T* token) { template, type_string, escaped, - token->range->from, - token->range->to, - token->location->start->line, - token->location->start->column, - token->location->end->line, - token->location->end->column + token->range.from, + token->range.to, + token->location.start.line, + token->location.start.column, + token->location.end.line, + token->location.end.column ); free(escaped); @@ -122,24 +128,24 @@ char* token_to_json(const token_T* token) { buffer_T range = buffer_new(); json_start_array(&json, "range"); - json_add_size_t(&range, NULL, token->range->from); - json_add_size_t(&range, NULL, token->range->to); + json_add_size_t(&range, NULL, token->range.from); + json_add_size_t(&range, NULL, token->range.to); buffer_concat(&json, &range); buffer_free(&range); json_end_array(&json); buffer_T start = buffer_new(); json_start_object(&json, "start"); - json_add_size_t(&start, "line", token->location->start->line); - json_add_size_t(&start, "column", token->location->start->column); + json_add_size_t(&start, "line", token->location.start.line); + json_add_size_t(&start, "column", token->location.start.column); buffer_concat(&json, &start); buffer_free(&start); json_end_object(&json); buffer_T end = buffer_new(); json_start_object(&json, "end"); - json_add_size_t(&end, "line", token->location->end->line); - json_add_size_t(&end, "column", token->location->end->column); + json_add_size_t(&end, "line", token->location.end.line); + json_add_size_t(&end, "column", token->location.end.column); buffer_concat(&json, &end); buffer_free(&end); json_end_object(&json); @@ -157,14 +163,6 @@ int token_type(const token_T* token) { return token->type; } -position_T* token_start_position(token_T* token) { - return token->location->start; -} - -position_T* token_end_position(token_T* token) { - return token->location->end; -} - token_T* token_copy(token_T* token) { if (!token) { return NULL; } @@ -184,8 +182,8 @@ token_T* token_copy(token_T* token) { } new_token->type = token->type; - new_token->range = range_copy(token->range); - new_token->location = location_copy(token->location); + new_token->range = token->range; + new_token->location = token->location; return new_token; } @@ -194,8 +192,6 @@ void token_free(token_T* token) { if (!token) { return; } if (token->value != NULL) { free(token->value); } - if (token->range != NULL) { range_free(token->range); } - if (token->location != NULL) { location_free(token->location); } free(token); } diff --git a/templates/src/ast_nodes.c.erb b/templates/src/ast_nodes.c.erb index c89debea1..3c6bcebcc 100644 --- a/templates/src/ast_nodes.c.erb +++ b/templates/src/ast_nodes.c.erb @@ -13,7 +13,7 @@ <%- nodes.each do |node| -%> <%- node_arguments = node.fields.any? ? node.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%> -<%- arguments = node_arguments + ["position_T* start_position", "position_T* end_position", "array_T* errors"] -%> +<%- arguments = node_arguments + ["position_T start_position", "position_T end_position", "array_T* errors"] -%> <%= node.struct_type %>* ast_<%= node.human %>_init(<%= arguments.join(", ") %>) { <%= node.struct_type %>* <%= node.human %> = malloc(sizeof(<%= node.struct_type %>)); @@ -81,8 +81,6 @@ void ast_free_base_node(AST_NODE_T* node) { array_free(&node->errors); } - if (node->location) { location_free(node->location); } - free(node); } diff --git a/templates/src/errors.c.erb b/templates/src/errors.c.erb index a3336a711..edf63a9e2 100644 --- a/templates/src/errors.c.erb +++ b/templates/src/errors.c.erb @@ -17,15 +17,16 @@ size_t error_sizeof(void) { return sizeof(struct ERROR_STRUCT); } -void error_init(ERROR_T* error, const error_type_T type, position_T* start, position_T* end) { +void error_init(ERROR_T* error, const error_type_T type, position_T start, position_T end) { if (!error) { return; } error->type = type; - error->location = location_init(position_copy(start), position_copy(end)); + error->location.start = start; + error->location.end = end; } <%- errors.each do |error| -%> <%- error_arguments = error.fields.any? ? error.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%> -<%- arguments = error_arguments + ["position_T* start", "position_T* end"] -%> +<%- arguments = error_arguments + ["position_T start", "position_T end"] -%> <%= error.struct_type %>* <%= error.human %>_init(<%= arguments.join(", ") %>) { <%= error.struct_type %>* <%= error.human %> = malloc(sizeof(<%= error.struct_type %>)); @@ -72,7 +73,7 @@ void error_init(ERROR_T* error, const error_type_T type, position_T* start, posi <%- error.fields.each do |field| -%> <%- case field -%> <%- when Herb::Template::PositionField -%> - <%= error.human %>-><%= field.name %> = position_copy(<%= field.name %>); + <%= error.human %>-><%= field.name %> = <%= field.name %>; <%- when Herb::Template::TokenField -%> <%= error.human %>-><%= field.name %> = token_copy(<%= field.name %>); <%- when Herb::Template::TokenTypeField -%> @@ -116,7 +117,6 @@ const char* error_human_type(ERROR_T* error) { void error_free_base_error(ERROR_T* error) { if (error == NULL) { return; } - if (error->location != NULL) { location_free(error->location); } if (error->message != NULL) { free(error->message); } free(error); @@ -130,8 +130,6 @@ static void error_free_<%= error.human %>(<%= error.struct_type %>* <%= error.hu <%- end -%> <%- error.fields.each do |field| -%> <%- case field -%> - <%- when Herb::Template::PositionField -%> - if (<%= error.human %>-><%= field.name %> != NULL) { position_free(<%= error.human %>-><%= field.name %>); } <%- when Herb::Template::TokenField -%> if (<%= error.human %>-><%= field.name %> != NULL) { token_free(<%= error.human %>-><%= field.name %>); } <%- when Herb::Template::TokenTypeField -%> diff --git a/templates/src/include/ast_nodes.h.erb b/templates/src/include/ast_nodes.h.erb index 9b6dca00f..89a7a4736 100644 --- a/templates/src/include/ast_nodes.h.erb +++ b/templates/src/include/ast_nodes.h.erb @@ -20,7 +20,7 @@ typedef enum { typedef struct AST_NODE_STRUCT { ast_node_type_T type; - location_T* location; + location_T location; // maybe a range too? array_T* errors; } AST_NODE_T; @@ -36,7 +36,7 @@ typedef struct <%= node.struct_name %> { <%- nodes.each do |node| -%> <%- node_arguments = node.fields.any? ? node.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%> -<%- arguments = node_arguments + ["position_T* start_position", "position_T* end_position", "array_T* errors"] -%> +<%- arguments = node_arguments + ["position_T start_position", "position_T end_position", "array_T* errors"] -%> <%= node.struct_type %>* ast_<%= node.human %>_init(<%= arguments.join(", ") %>); <%- end -%> diff --git a/templates/src/include/errors.h.erb b/templates/src/include/errors.h.erb index 1baca9430..41a5ea068 100644 --- a/templates/src/include/errors.h.erb +++ b/templates/src/include/errors.h.erb @@ -16,7 +16,7 @@ typedef enum { typedef struct ERROR_STRUCT { error_type_T type; - location_T* location; + location_T location; char* message; } ERROR_T; @@ -31,12 +31,12 @@ typedef struct { <%- errors.each do |error| -%> <%- error_arguments = error.fields.any? ? error.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%> -<%- arguments = error_arguments + ["position_T* start", "position_T* end"] -%> +<%- arguments = error_arguments + ["position_T start", "position_T end"] -%> <%= error.struct_type %>* <%= error.human %>_init(<%= arguments.join(", ") %>); void append_<%= error.human %>(<%= (arguments << "array_T* errors").join(", ") %>); <%- end -%> -void error_init(ERROR_T* error, error_type_T type, position_T* start, position_T* end); +void error_init(ERROR_T* error, error_type_T type, position_T start, position_T end); size_t error_sizeof(void); error_type_T error_type(ERROR_T* error); diff --git a/wasm/extension_helpers.cpp b/wasm/extension_helpers.cpp index 490a81412..ecc59f2e0 100644 --- a/wasm/extension_helpers.cpp +++ b/wasm/extension_helpers.cpp @@ -27,44 +27,32 @@ val CreateString(const char* string) { return string ? val(string) : val::null(); } -val CreatePosition(position_T* position) { - if (!position) { - return val::null(); - } - +val CreatePosition(position_T position) { val Object = val::global("Object"); val result = Object.new_(); - result.set("line", position->line); - result.set("column", position->column); + result.set("line", position.line); + result.set("column", position.column); return result; } -val CreateLocation(location_T* location) { - if (!location) { - return val::null(); - } - +val CreateLocation(location_T location) { val Object = val::global("Object"); val result = Object.new_(); - result.set("start", CreatePosition(location->start)); - result.set("end", CreatePosition(location->end)); + result.set("start", CreatePosition(location.start)); + result.set("end", CreatePosition(location.end)); return result; } -val CreateRange(range_T* range) { - if (!range) { - return val::null(); - } - +val CreateRange(range_T range) { val Array = val::global("Array"); val result = Array.new_(); - result.call("push", range->from); - result.call("push", range->to); + result.call("push", range.from); + result.call("push", range.to); return result; } diff --git a/wasm/extension_helpers.h b/wasm/extension_helpers.h index 923df6f3b..86bf67100 100644 --- a/wasm/extension_helpers.h +++ b/wasm/extension_helpers.h @@ -14,9 +14,9 @@ extern "C" { } emscripten::val CreateString(const char* string); -emscripten::val CreatePosition(position_T* position); -emscripten::val CreateLocation(location_T* location); -emscripten::val CreateRange(range_T* range); +emscripten::val CreatePosition(position_T position); +emscripten::val CreateLocation(location_T location); +emscripten::val CreateRange(range_T range); emscripten::val CreateToken(token_T* token); emscripten::val CreateLexResult(array_T* tokens, const std::string& source); emscripten::val CreateParseResult(AST_DOCUMENT_NODE_T *root, const std::string& source); From 9517614fc1d2c69235913642f4f83e8e4c36876e Mon Sep 17 00:00:00 2001 From: Drew Hoffer <55151884+drewhoffer@users.noreply.github.com> Date: Sun, 5 Oct 2025 01:11:07 -0400 Subject: [PATCH 13/97] Linter: Fix `--version` flag for CLI (#488) Closes #437 --------- Co-authored-by: Marco Roth --- javascript/packages/linter/src/cli.ts | 4 +++- javascript/packages/linter/src/cli/argument-parser.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/javascript/packages/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts index a9b32f1de..eb3fbfc00 100644 --- a/javascript/packages/linter/src/cli.ts +++ b/javascript/packages/linter/src/cli.ts @@ -124,7 +124,7 @@ export class CLI { } protected async beforeProcess(): Promise { - await Herb.load() + // Hook for subclasses to add custom output before processing } protected async afterProcess(_results: any, _outputOptions: any): Promise { @@ -132,6 +132,8 @@ export class CLI { } async run() { + await Herb.load() + const startTime = Date.now() const startDate = new Date() diff --git a/javascript/packages/linter/src/cli/argument-parser.ts b/javascript/packages/linter/src/cli/argument-parser.ts index c25337b08..8389119c1 100644 --- a/javascript/packages/linter/src/cli/argument-parser.ts +++ b/javascript/packages/linter/src/cli/argument-parser.ts @@ -9,7 +9,7 @@ import { Herb } from "@herb-tools/node-wasm" import { THEME_NAMES, DEFAULT_THEME } from "@herb-tools/highlighter" import type { ThemeInput } from "@herb-tools/highlighter" -import { name, version } from "../../package.json" +import { name, version, dependencies } from "../../package.json" export type FormatOption = "simple" | "detailed" | "json" @@ -74,7 +74,9 @@ export class ArgumentParser { if (values.version) { console.log("Versions:") - console.log(` ${name}@${version}, ${Herb.version}`.split(", ").join("\n ")) + console.log(` ${name}@${version}`) + console.log(` @herb-tools/printer@${dependencies["@herb-tools/printer"]}`) + console.log(` ${Herb.version}`.split(", ").join("\n ")) process.exit(0) } From ea545add32ee8f5fd8aaa08e469885d165677bb0 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 5 Oct 2025 14:37:40 +0900 Subject: [PATCH 14/97] Formatter: Print `Experimental Preview` warning on `stderr` (#575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the formatter CLI to print the `⚠️ Experimental Preview ...` warning on `stderr` instead of `stdout` to other tools can programmatically use the formatter output. Resolves https://github.com/marcoroth/herb/issues/574 --- javascript/packages/formatter/src/cli.ts | 4 +- .../packages/formatter/test/cli.test.ts | 58 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/javascript/packages/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts index 5d88c1d28..a6502833d 100644 --- a/javascript/packages/formatter/src/cli.ts +++ b/javascript/packages/formatter/src/cli.ts @@ -112,8 +112,8 @@ export class CLI { process.exit(0) } - console.log("⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md") - console.log() + console.error("⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md") + console.error() const formatOptions = resolveFormatOptions({ indentWidth, diff --git a/javascript/packages/formatter/test/cli.test.ts b/javascript/packages/formatter/test/cli.test.ts index 5d4d3ed46..b45f7d49c 100644 --- a/javascript/packages/formatter/test/cli.test.ts +++ b/javascript/packages/formatter/test/cli.test.ts @@ -155,20 +155,41 @@ describe("CLI Binary", () => { } }) - it("should show experimental preview message", async () => { + it("should show experimental preview message on stderr", async () => { const result = await execBinary([]) - expect(result.stdout).toContain("⚠️ Experimental Preview") - expect(result.stdout).toContain("early development") - expect(result.stdout).toContain("github.com/marcoroth/herb/issues") + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stderr).toContain("early development") + expect(result.stderr).toContain("github.com/marcoroth/herb/issues") + }) + + it("should not include experimental preview message in stdout", async () => { + const input = '
    test
    ' + const result = await execBinary([], input) + + expectExitCode(result, 0) + expect(result.stdout).not.toContain("⚠️ Experimental Preview") + expect(result.stdout).toContain('
    test
    ') + }) + + it("stdout should contain only formatted output without warnings", async () => { + const input = '

    Hello

    ' + const result = await execBinary([], input) + + expectExitCode(result, 0) + + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stdout).not.toContain("⚠️") + expect(result.stdout).not.toContain("Experimental") + expect(result.stdout).toBe('
    \n

    Hello

    \n
    \n') }) it("should format empty input from stdin when no args provided", async () => { const result = await execBinary([], "") expectExitCode(result, 0) - expect(result.stdout).toContain("⚠️ Experimental Preview") - expect(result.stdout).toContain("\n\n") + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stdout).toBe("\n") }) it("should handle no files found in empty directory", async () => { @@ -178,7 +199,7 @@ describe("CLI Binary", () => { const result = await execBinary(["test-empty-dir"]) expectExitCode(result, 0) - expect(result.stdout).toContain("⚠️ Experimental Preview") + expect(result.stderr).toContain("⚠️ Experimental Preview") expect(result.stdout).toContain("No files found matching pattern:") expect(result.stdout).toContain("test-empty-dir/**/*.html.erb") } finally { @@ -365,12 +386,8 @@ describe("CLI Binary", () => { expectExitCode(result, 0) expect(result.stdout.endsWith('\n')).toBe(true) - expect(result.stdout).toContain("⚠️ Experimental Preview") - expect(result.stdout).toContain('
    Hello
    ') - - const lines = result.stdout.split('\n') - const formattedLines = lines.slice(2) // Skip experimental preview lines - expect(formattedLines.join('\n')).toBe('
    Hello
    \n') + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stdout).toBe('
    Hello
    \n') }) it("CLI should preserve existing trailing newline", async () => { @@ -379,12 +396,8 @@ describe("CLI Binary", () => { expectExitCode(result, 0) expect(result.stdout.endsWith('\n')).toBe(true) - expect(result.stdout).toContain("⚠️ Experimental Preview") - expect(result.stdout).toContain('
    Hello
    ') - - const lines = result.stdout.split('\n') - const formattedLines = lines.slice(2) // Skip experimental preview lines - expect(formattedLines.join('\n')).toBe('
    Hello
    \n') + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stdout).toBe('
    Hello
    \n') }) it("CLI should add trailing newline to empty input", async () => { @@ -392,11 +405,8 @@ describe("CLI Binary", () => { const result = await execBinary([], input) expectExitCode(result, 0) - expect(result.stdout).toContain("⚠️ Experimental Preview") - - const lines = result.stdout.split('\n') - const formattedLines = lines.slice(2) // Skip experimental preview lines - expect(formattedLines.join('\n')).toBe('\n') + expect(result.stderr).toContain("⚠️ Experimental Preview") + expect(result.stdout).toBe('\n') }) it("should show --indent-width option in help", async () => { From 2ca92ab0144c1377e20fcff4084a34584970e590 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 5 Oct 2025 15:07:07 +0900 Subject: [PATCH 15/97] Engine: Support Ruby Block Comments when compiling templates (#576) This pull request updates the Engine to detect and support the compilation of [Ruby Block Comments](https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html) in HTML+ERB templates. The following templates can now be compiled and evaluated: ```html+erb <% =begin %> This, while unusual, is a legal form of commenting. <% =end %>
    Hey there
    ``` Resolves https://github.com/marcoroth/herb/issues/562 --- examples/block_comment.html.erb | 7 ++ lib/herb/engine.rb | 16 ++-- test/engine/block_comments_test.rb | 95 +++++++++++++++++++ ...iline_ff404c4e708f59f532b4042441e458ad.txt | 11 +++ ..._tags_95bcfa3f3e28634bf923f18df0cd38a1.txt | 9 ++ ...after_0ee0ba8a9a25359175b884cf66f37b2c.txt | 12 +++ ...iline_6bf3affa07b559b73f6428e0dbd270d2.txt | 2 + ..._tags_8a842d797202d0b31c9832cd43a22686.txt | 2 + ...after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt | 4 + ...ation_d5c98b9f230e001f8aabf838d3774698.txt | 12 +++ ...tion_3ee70e5b90c1ff368a7783c49c6dc611.txt} | 0 ...tion_da96aac1987dbb33d41eb4a121419b47.txt} | 0 ...tion_2b3c4e3c244db77468813a7472298e82.txt} | 0 ...tion_8c18fb8399a2fd1e65b0333bc01e041c.txt} | 0 ...tion_0109f6af5a474973b8b1b52636135c4f.txt} | 0 ...tion_eeecb969def01ab8005c7b9f23686fed.txt} | 0 ...tion_1437d0425640509570342c24b30de571.txt} | 0 ...tion_ba78b92bec7a4692a2043f2ba2ebe057.txt} | 0 ...tion_4ddb2a17755d3e775e3225970bc60a96.txt} | 0 ...tion_aa8aed9ef61b138a28efed42f4b72251.txt} | 0 ...tion_1729fad3a77618acdc687c9fb671b75b.txt} | 0 ...tion_d81de4ca83482c4836215ef7177d9eec.txt} | 0 ...tion_9c22f391d1d03fa66b3d18095354a236.txt} | 0 ...tion_0b1269c00fd15a74df125278ed6a9fc4.txt} | 0 ...tion_45fa7aa654c0dc06d1a1b9504002dfba.txt} | 0 ...tion_a705eb5ed83b4db368d7204baa136b36.txt} | 0 26 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 examples/block_comment.html.erb create mode 100644 test/engine/block_comments_test.rb create mode 100644 test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt create mode 100644 test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt create mode 100644 test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt create mode 100644 test/snapshots/engine/block_comments_test/test_0004_evaluation:_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt create mode 100644 test/snapshots/engine/block_comments_test/test_0005_evaluation:_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt create mode 100644 test/snapshots/engine/block_comments_test/test_0006_evaluation:_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt create mode 100644 test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt rename test/snapshots/engine/examples_compilation_test/{test_0004_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt => test_0005_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0005_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt => test_0006_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0006_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt => test_0007_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0007_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt => test_0008_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0008_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt => test_0009_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0009_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt => test_0010_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0010_if_else_compilation_1437d0425640509570342c24b30de571.txt => test_0011_if_else_compilation_1437d0425640509570342c24b30de571.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0011_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt => test_0012_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0012_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt => test_0013_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0013_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt => test_0014_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0014_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt => test_0015_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0015_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt => test_0016_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0016_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt => test_0017_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0017_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt => test_0018_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0018_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt => test_0019_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt} (100%) rename test/snapshots/engine/examples_compilation_test/{test_0019_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt => test_0020_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt} (100%) diff --git a/examples/block_comment.html.erb b/examples/block_comment.html.erb new file mode 100644 index 000000000..c424fd687 --- /dev/null +++ b/examples/block_comment.html.erb @@ -0,0 +1,7 @@ +<% +=begin %> + This, while unusual, is a legal form of commenting. +<% +=end %> + +
    Content
    diff --git a/lib/herb/engine.rb b/lib/herb/engine.rb index 133db20d7..c4fe7752b 100644 --- a/lib/herb/engine.rb +++ b/lib/herb/engine.rb @@ -193,13 +193,17 @@ def add_text(text) def add_code(code) terminate_expression - @src << " " << code - - # TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST - if code.include?("#") - @src << "\n" + if code.include?("=begin") || code.include?("=end") + @src << "\n" << code << "\n" else - @src << ";" unless code[-1] == "\n" + @src << " " << code + + # TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST + if code.include?("#") + @src << "\n" + else + @src << ";" unless code[-1] == "\n" + end end @buffer_on_stack = false diff --git a/test/engine/block_comments_test.rb b/test/engine/block_comments_test.rb new file mode 100644 index 000000000..b179f7854 --- /dev/null +++ b/test/engine/block_comments_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative "../test_helper" +require_relative "../snapshot_utils" +require_relative "../../lib/herb/engine" + +module Engine + class BlockCommentsTest < Minitest::Spec + include SnapshotUtils + + test "ruby block comments with =begin and =end multiline" do + template = <<~ERB + <% + =begin %> + This, while unusual, is a legal form of commenting. + <% + =end %> +
    Hey there
    + ERB + + assert_compiled_snapshot(template) + end + + test "ruby block comments inside erb tags" do + template = <<~ERB + <% + =begin + This is a comment + =end + %> +

    Content

    + ERB + + assert_compiled_snapshot(template) + end + + test "ruby block comments with code before and after" do + template = <<~ERB + <% x = 1 %> + <% + =begin + Multi-line comment + spanning multiple lines + =end + %> + <% y = 2 %> +
    <%= x + y %>
    + ERB + + assert_compiled_snapshot(template) + end + + test "evaluation: ruby block comments with =begin and =end mutliline" do + template = <<~ERB + <% + =begin %> + This, while unusual, is a legal form of commenting. + <% + =end %> +
    Hey there
    + ERB + + assert_evaluated_snapshot(template) + end + + test "evaluation: ruby block comments inside erb tags" do + template = <<~ERB + <% + =begin + This is a comment + =end + %> +

    Content

    + ERB + + assert_evaluated_snapshot(template) + end + + test "evaluation: ruby block comments with code before and after" do + template = <<~ERB + <% x = 1 %> + <% + =begin + Multi-line comment + spanning multiple lines + =end + %> + <% y = 2 %> +
    <%= x + y %>
    + ERB + + assert_evaluated_snapshot(template, { x: 1, y: 2 }) + end + end +end diff --git a/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt b/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt new file mode 100644 index 000000000..b86d395f2 --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt @@ -0,0 +1,11 @@ +_buf = ::String.new; + +=begin + _buf << ' + This, while unusual, is a legal form of commenting. +'.freeze; +=end + _buf << ' +
    Hey there
    +'.freeze; +_buf.to_s diff --git a/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt b/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt new file mode 100644 index 000000000..467a7f23a --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt @@ -0,0 +1,9 @@ +_buf = ::String.new; + +=begin +This is a comment +=end + _buf << ' +

    Content

    +'.freeze; +_buf.to_s diff --git a/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt b/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt new file mode 100644 index 000000000..545dc4e42 --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt @@ -0,0 +1,12 @@ +_buf = ::String.new; + x = 1; _buf << ' +'.freeze; +=begin +Multi-line comment +spanning multiple lines +=end + _buf << ' +'.freeze; y = 2; _buf << ' +
    '.freeze; _buf << (x + y).to_s; _buf << '
    +'.freeze; +_buf.to_s diff --git a/test/snapshots/engine/block_comments_test/test_0004_evaluation:_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt b/test/snapshots/engine/block_comments_test/test_0004_evaluation:_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt new file mode 100644 index 000000000..a6a4497b5 --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0004_evaluation:_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt @@ -0,0 +1,2 @@ + +
    Hey there
    diff --git a/test/snapshots/engine/block_comments_test/test_0005_evaluation:_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt b/test/snapshots/engine/block_comments_test/test_0005_evaluation:_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt new file mode 100644 index 000000000..1efa30582 --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0005_evaluation:_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt @@ -0,0 +1,2 @@ + +

    Content

    diff --git a/test/snapshots/engine/block_comments_test/test_0006_evaluation:_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt b/test/snapshots/engine/block_comments_test/test_0006_evaluation:_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt new file mode 100644 index 000000000..d630b5b60 --- /dev/null +++ b/test/snapshots/engine/block_comments_test/test_0006_evaluation:_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt @@ -0,0 +1,4 @@ + + + +
    3
    diff --git a/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt b/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt new file mode 100644 index 000000000..dd6f62d48 --- /dev/null +++ b/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt @@ -0,0 +1,12 @@ +_buf = ::String.new; + +=begin + _buf << ' + This, while unusual, is a legal form of commenting. +'.freeze; +=end + _buf << ' + +
    Content
    +'.freeze; +_buf.to_s diff --git a/test/snapshots/engine/examples_compilation_test/test_0004_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt b/test/snapshots/engine/examples_compilation_test/test_0005_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0004_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt rename to test/snapshots/engine/examples_compilation_test/test_0005_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0005_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt b/test/snapshots/engine/examples_compilation_test/test_0006_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0005_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt rename to test/snapshots/engine/examples_compilation_test/test_0006_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0006_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt b/test/snapshots/engine/examples_compilation_test/test_0007_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0006_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt rename to test/snapshots/engine/examples_compilation_test/test_0007_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0007_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt b/test/snapshots/engine/examples_compilation_test/test_0008_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0007_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt rename to test/snapshots/engine/examples_compilation_test/test_0008_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0008_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt b/test/snapshots/engine/examples_compilation_test/test_0009_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0008_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt rename to test/snapshots/engine/examples_compilation_test/test_0009_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0009_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt b/test/snapshots/engine/examples_compilation_test/test_0010_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0009_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt rename to test/snapshots/engine/examples_compilation_test/test_0010_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0010_if_else_compilation_1437d0425640509570342c24b30de571.txt b/test/snapshots/engine/examples_compilation_test/test_0011_if_else_compilation_1437d0425640509570342c24b30de571.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0010_if_else_compilation_1437d0425640509570342c24b30de571.txt rename to test/snapshots/engine/examples_compilation_test/test_0011_if_else_compilation_1437d0425640509570342c24b30de571.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0011_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt b/test/snapshots/engine/examples_compilation_test/test_0012_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0011_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt rename to test/snapshots/engine/examples_compilation_test/test_0012_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0012_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt b/test/snapshots/engine/examples_compilation_test/test_0013_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0012_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt rename to test/snapshots/engine/examples_compilation_test/test_0013_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0013_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt b/test/snapshots/engine/examples_compilation_test/test_0014_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0013_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt rename to test/snapshots/engine/examples_compilation_test/test_0014_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0014_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt b/test/snapshots/engine/examples_compilation_test/test_0015_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0014_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt rename to test/snapshots/engine/examples_compilation_test/test_0015_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0015_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt b/test/snapshots/engine/examples_compilation_test/test_0016_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0015_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt rename to test/snapshots/engine/examples_compilation_test/test_0016_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0016_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt b/test/snapshots/engine/examples_compilation_test/test_0017_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0016_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt rename to test/snapshots/engine/examples_compilation_test/test_0017_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0017_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt b/test/snapshots/engine/examples_compilation_test/test_0018_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0017_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt rename to test/snapshots/engine/examples_compilation_test/test_0018_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0018_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt b/test/snapshots/engine/examples_compilation_test/test_0019_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0018_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt rename to test/snapshots/engine/examples_compilation_test/test_0019_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt diff --git a/test/snapshots/engine/examples_compilation_test/test_0019_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt b/test/snapshots/engine/examples_compilation_test/test_0020_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt similarity index 100% rename from test/snapshots/engine/examples_compilation_test/test_0019_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt rename to test/snapshots/engine/examples_compilation_test/test_0020_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt From 161fc1800703bd8ae78d1a0e5e78e08d7af3d0e9 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 5 Oct 2025 15:53:54 +0900 Subject: [PATCH 16/97] Parser: Fix analysis of nested `case/when` and `case/in` parsing (#578) This pull request updates the parser to fix the analysis of nested control flow structures within `case/when` and `case/in` statements. The following kind templates now have the properly nested structure: ```html+erb <% case 1 %> <% when 1 %> <%= content_tag(:p) do %> Yep <% end %> <% end %> ``` Resolves https://github.com/marcoroth/herb/issues/540 --- src/analyze.c | 38 +--------- test/analyze/case_in_test.rb | 36 +++++++++ test/analyze/case_test.rb | 64 ++++++++++++++++ ...lause_a5d7393ef0ab780cd777fd4d09413ac8.txt | 48 ++++++++++++ ...lause_8cdfe954fcb68cf29f71d5c05893547c.txt | 66 ++++++++++++++++ ...lause_51fcf7566215a5284ae1ee34c920837e.txt | 49 ++++++++++++ ..._when_adb20c2773fe1206d2b47123f8001e22.txt | 48 ++++++++++++ ..._when_05f7d387314a95620a58ddc7f7d31b7b.txt | 66 ++++++++++++++++ ..._when_6e70c1bf60788969969296d35c409a47.txt | 66 ++++++++++++++++ ...auses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt | 75 +++++++++++++++++++ ..._when_ccc0bd511203c41d9274ccc360e736c2.txt | 49 ++++++++++++ 11 files changed, 569 insertions(+), 36 deletions(-) create mode 100644 test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt create mode 100644 test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt create mode 100644 test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt create mode 100644 test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt create mode 100644 test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt create mode 100644 test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt create mode 100644 test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt create mode 100644 test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt diff --git a/src/analyze.c b/src/analyze.c index 5ed8215db..bd0778520 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -435,24 +435,7 @@ static size_t process_control_structure( array_T* when_statements = array_init(8); index++; - while (index < array_size(array)) { - AST_NODE_T* child = array_get(array, index); - - if (!child) { break; } - - if (child->type == AST_ERB_CONTENT_NODE) { - AST_ERB_CONTENT_NODE_T* child_erb = (AST_ERB_CONTENT_NODE_T*) child; - control_type_t child_type = detect_control_type(child_erb); - - if (child_type == CONTROL_TYPE_WHEN || child_type == CONTROL_TYPE_IN || child_type == CONTROL_TYPE_ELSE - || child_type == CONTROL_TYPE_END) { - break; - } - } - - array_append(when_statements, child); - index++; - } + index = process_block_children(node, array, index, when_statements, context, CONTROL_TYPE_WHEN); AST_ERB_WHEN_NODE_T* when_node = ast_erb_when_node_init( erb_content->tag_opening, @@ -471,24 +454,7 @@ static size_t process_control_structure( array_T* in_statements = array_init(8); index++; - while (index < array_size(array)) { - AST_NODE_T* child = array_get(array, index); - - if (!child) { break; } - - if (child->type == AST_ERB_CONTENT_NODE) { - AST_ERB_CONTENT_NODE_T* child_erb = (AST_ERB_CONTENT_NODE_T*) child; - control_type_t child_type = detect_control_type(child_erb); - - if (child_type == CONTROL_TYPE_IN || child_type == CONTROL_TYPE_WHEN || child_type == CONTROL_TYPE_ELSE - || child_type == CONTROL_TYPE_END) { - break; - } - } - - array_append(in_statements, child); - index++; - } + index = process_block_children(node, array, index, in_statements, context, CONTROL_TYPE_IN); AST_ERB_IN_NODE_T* in_node = ast_erb_in_node_init( erb_content->tag_opening, diff --git a/test/analyze/case_in_test.rb b/test/analyze/case_in_test.rb index a648ada19..8fb1a25ac 100644 --- a/test/analyze/case_in_test.rb +++ b/test/analyze/case_in_test.rb @@ -92,5 +92,41 @@ class CaseInTest < Minitest::Spec <% end %> HTML end + + test "case in with block inside in clause" do + assert_parsed_snapshot(<<~HTML) + <% case result %> + <% in { status: :success } %> + <%= content_tag(:div) do %> + Success! + <% end %> + <% end %> + HTML + end + + test "case in with multiple blocks in in clause" do + assert_parsed_snapshot(<<~HTML) + <% case data %> + <% in { type: :alert } %> + <%= content_tag(:div) do %> + Alert + <% end %> + <%= content_tag(:span) do %> + Icon + <% end %> + <% end %> + HTML + end + + test "case in with if statement inside in clause" do + assert_parsed_snapshot(<<~HTML) + <% case value %> + <% in [Integer] %> + <% if positive? %> + Positive + <% end %> + <% end %> + HTML + end end end diff --git a/test/analyze/case_test.rb b/test/analyze/case_test.rb index 8a7d2b538..0e181c0b4 100644 --- a/test/analyze/case_test.rb +++ b/test/analyze/case_test.rb @@ -90,5 +90,69 @@ class CaseTest < Minitest::Spec <% end %> HTML end + + test "case with block inside when" do + assert_parsed_snapshot(<<~HTML) + <% case 1 %> + <% when 1 %> + <%= content_tag(:p) do %> + Yep + <% end %> + <% end %> + HTML + end + + test "case with multiple blocks in when" do + assert_parsed_snapshot(<<~HTML) + <% case status %> + <% when :active %> + <%= content_tag(:div) do %> + Active + <% end %> + <%= content_tag(:span) do %> + Badge + <% end %> + <% end %> + HTML + end + + test "case with nested blocks in when" do + assert_parsed_snapshot(<<~HTML) + <% case level %> + <% when 1 %> + <%= content_tag(:div) do %> + <%= content_tag(:p) do %> + Nested + <% end %> + <% end %> + <% end %> + HTML + end + + test "case with block in multiple when clauses" do + assert_parsed_snapshot(<<~HTML) + <% case type %> + <% when :a %> + <%= form_for(obj) do |f| %> + Form A + <% end %> + <% when :b %> + <%= form_for(obj) do |f| %> + Form B + <% end %> + <% end %> + HTML + end + + test "case with if statement inside when" do + assert_parsed_snapshot(<<~HTML) + <% case status %> + <% when :active %> + <% if admin? %> + Admin view + <% end %> + <% end %> + HTML + end end end diff --git a/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt b/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt new file mode 100644 index 000000000..c29867eb1 --- /dev/null +++ b/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt @@ -0,0 +1,48 @@ +@ DocumentNode (location: (1:0)-(7:0)) +└── children: (2 items) + ├── @ ERBCaseMatchNode (location: (1:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case result " (location: (1:2)-(1:15)) + │ ├── tag_closing: "%>" (location: (1:15)-(1:17)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:17)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBInNode (location: (2:0)-(2:29)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " in { status: :success } " (location: (2:2)-(2:27)) + │ │ ├── tag_closing: "%>" (location: (2:27)-(2:29)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:29)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27)) + │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2)) + │ │ │ │ └── content: "\n Success!\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (5:11)-(6:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (6:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (6:0)-(6:2)) + │ ├── content: " end " (location: (6:2)-(6:7)) + │ └── tag_closing: "%>" (location: (6:7)-(6:9)) + │ + │ + └── @ HTMLTextNode (location: (6:9)-(7:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt b/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt new file mode 100644 index 000000000..15df4180f --- /dev/null +++ b/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt @@ -0,0 +1,66 @@ +@ DocumentNode (location: (1:0)-(10:0)) +└── children: (2 items) + ├── @ ERBCaseMatchNode (location: (1:0)-(9:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case data " (location: (1:2)-(1:13)) + │ ├── tag_closing: "%>" (location: (1:13)-(1:15)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:15)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBInNode (location: (2:0)-(2:25)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " in { type: :alert } " (location: (2:2)-(2:23)) + │ │ ├── tag_closing: "%>" (location: (2:23)-(2:25)) + │ │ └── statements: (5 items) + │ │ ├── @ HTMLTextNode (location: (2:25)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27)) + │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2)) + │ │ │ │ └── content: "\n Alert\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (6:2)-(8:11)) + │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5)) + │ │ │ ├── content: " content_tag(:span) do " (location: (6:5)-(6:28)) + │ │ │ ├── tag_closing: "%>" (location: (6:28)-(6:30)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (6:30)-(8:2)) + │ │ │ │ └── content: "\n Icon\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (8:2)-(8:11)) + │ │ │ ├── tag_opening: "<%" (location: (8:2)-(8:4)) + │ │ │ ├── content: " end " (location: (8:4)-(8:9)) + │ │ │ └── tag_closing: "%>" (location: (8:9)-(8:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (8:11)-(9:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (9:0)-(9:9)) + │ ├── tag_opening: "<%" (location: (9:0)-(9:2)) + │ ├── content: " end " (location: (9:2)-(9:7)) + │ └── tag_closing: "%>" (location: (9:7)-(9:9)) + │ + │ + └── @ HTMLTextNode (location: (9:9)-(10:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt b/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt new file mode 100644 index 000000000..5acba3aa5 --- /dev/null +++ b/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt @@ -0,0 +1,49 @@ +@ DocumentNode (location: (1:0)-(7:0)) +└── children: (2 items) + ├── @ ERBCaseMatchNode (location: (1:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case value " (location: (1:2)-(1:14)) + │ ├── tag_closing: "%>" (location: (1:14)-(1:16)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:16)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBInNode (location: (2:0)-(2:18)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " in [Integer] " (location: (2:2)-(2:16)) + │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBIfNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (3:2)-(3:4)) + │ │ │ ├── content: " if positive? " (location: (3:4)-(3:18)) + │ │ │ ├── tag_closing: "%>" (location: (3:18)-(3:20)) + │ │ │ ├── statements: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:20)-(5:2)) + │ │ │ │ └── content: "\n Positive\n " + │ │ │ │ + │ │ │ ├── subsequent: ∅ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (5:11)-(6:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (6:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (6:0)-(6:2)) + │ ├── content: " end " (location: (6:2)-(6:7)) + │ └── tag_closing: "%>" (location: (6:7)-(6:9)) + │ + │ + └── @ HTMLTextNode (location: (6:9)-(7:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt b/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt new file mode 100644 index 000000000..e2e329787 --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt @@ -0,0 +1,48 @@ +@ DocumentNode (location: (1:0)-(7:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case 1 " (location: (1:2)-(1:10)) + │ ├── tag_closing: "%>" (location: (1:10)-(1:12)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:12)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (2:0)-(2:12)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " when 1 " (location: (2:2)-(2:10)) + │ │ ├── tag_closing: "%>" (location: (2:10)-(2:12)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:12)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ ├── content: " content_tag(:p) do " (location: (3:5)-(3:25)) + │ │ │ ├── tag_closing: "%>" (location: (3:25)-(3:27)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:27)-(5:2)) + │ │ │ │ └── content: "\n Yep\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (5:11)-(6:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (6:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (6:0)-(6:2)) + │ ├── content: " end " (location: (6:2)-(6:7)) + │ └── tag_closing: "%>" (location: (6:7)-(6:9)) + │ + │ + └── @ HTMLTextNode (location: (6:9)-(7:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt b/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt new file mode 100644 index 000000000..ff5e466ab --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt @@ -0,0 +1,66 @@ +@ DocumentNode (location: (1:0)-(10:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(9:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case status " (location: (1:2)-(1:15)) + │ ├── tag_closing: "%>" (location: (1:15)-(1:17)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:17)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (2:0)-(2:18)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " when :active " (location: (2:2)-(2:16)) + │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18)) + │ │ └── statements: (5 items) + │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27)) + │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2)) + │ │ │ │ └── content: "\n Active\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (6:2)-(8:11)) + │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5)) + │ │ │ ├── content: " content_tag(:span) do " (location: (6:5)-(6:28)) + │ │ │ ├── tag_closing: "%>" (location: (6:28)-(6:30)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (6:30)-(8:2)) + │ │ │ │ └── content: "\n Badge\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (8:2)-(8:11)) + │ │ │ ├── tag_opening: "<%" (location: (8:2)-(8:4)) + │ │ │ ├── content: " end " (location: (8:4)-(8:9)) + │ │ │ └── tag_closing: "%>" (location: (8:9)-(8:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (8:11)-(9:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (9:0)-(9:9)) + │ ├── tag_opening: "<%" (location: (9:0)-(9:2)) + │ ├── content: " end " (location: (9:2)-(9:7)) + │ └── tag_closing: "%>" (location: (9:7)-(9:9)) + │ + │ + └── @ HTMLTextNode (location: (9:9)-(10:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt b/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt new file mode 100644 index 000000000..857244cb8 --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt @@ -0,0 +1,66 @@ +@ DocumentNode (location: (1:0)-(9:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(8:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case level " (location: (1:2)-(1:14)) + │ ├── tag_closing: "%>" (location: (1:14)-(1:16)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:16)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (2:0)-(2:12)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " when 1 " (location: (2:2)-(2:10)) + │ │ ├── tag_closing: "%>" (location: (2:10)-(2:12)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:12)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (3:2)-(7:11)) + │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27)) + │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29)) + │ │ │ ├── body: (3 items) + │ │ │ │ ├── @ HTMLTextNode (location: (3:29)-(4:4)) + │ │ │ │ │ └── content: "\n " + │ │ │ │ │ + │ │ │ │ ├── @ ERBBlockNode (location: (4:4)-(6:13)) + │ │ │ │ │ ├── tag_opening: "<%=" (location: (4:4)-(4:7)) + │ │ │ │ │ ├── content: " content_tag(:p) do " (location: (4:7)-(4:27)) + │ │ │ │ │ ├── tag_closing: "%>" (location: (4:27)-(4:29)) + │ │ │ │ │ ├── body: (1 item) + │ │ │ │ │ │ └── @ HTMLTextNode (location: (4:29)-(6:4)) + │ │ │ │ │ │ └── content: "\n Nested\n " + │ │ │ │ │ │ + │ │ │ │ │ └── end_node: + │ │ │ │ │ └── @ ERBEndNode (location: (6:4)-(6:13)) + │ │ │ │ │ ├── tag_opening: "<%" (location: (6:4)-(6:6)) + │ │ │ │ │ ├── content: " end " (location: (6:6)-(6:11)) + │ │ │ │ │ └── tag_closing: "%>" (location: (6:11)-(6:13)) + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ └── @ HTMLTextNode (location: (6:13)-(7:2)) + │ │ │ │ └── content: "\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (7:2)-(7:11)) + │ │ │ ├── tag_opening: "<%" (location: (7:2)-(7:4)) + │ │ │ ├── content: " end " (location: (7:4)-(7:9)) + │ │ │ └── tag_closing: "%>" (location: (7:9)-(7:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (7:11)-(8:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (8:0)-(8:9)) + │ ├── tag_opening: "<%" (location: (8:0)-(8:2)) + │ ├── content: " end " (location: (8:2)-(8:7)) + │ └── tag_closing: "%>" (location: (8:7)-(8:9)) + │ + │ + └── @ HTMLTextNode (location: (8:9)-(9:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt b/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt new file mode 100644 index 000000000..818e209a5 --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt @@ -0,0 +1,75 @@ +@ DocumentNode (location: (1:0)-(11:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(10:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case type " (location: (1:2)-(1:13)) + │ ├── tag_closing: "%>" (location: (1:13)-(1:15)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:15)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (2 items) + │ │ ├── @ ERBWhenNode (location: (2:0)-(2:13)) + │ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ │ ├── content: " when :a " (location: (2:2)-(2:11)) + │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13)) + │ │ │ └── statements: (3 items) + │ │ │ ├── @ HTMLTextNode (location: (2:13)-(3:2)) + │ │ │ │ └── content: "\n " + │ │ │ │ + │ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11)) + │ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5)) + │ │ │ │ ├── content: " form_for(obj) do |f| " (location: (3:5)-(3:27)) + │ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29)) + │ │ │ │ ├── body: (1 item) + │ │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2)) + │ │ │ │ │ └── content: "\n Form A\n " + │ │ │ │ │ + │ │ │ │ └── end_node: + │ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ │ + │ │ │ │ + │ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ │ + │ │ └── @ ERBWhenNode (location: (6:0)-(6:13)) + │ │ ├── tag_opening: "<%" (location: (6:0)-(6:2)) + │ │ ├── content: " when :b " (location: (6:2)-(6:11)) + │ │ ├── tag_closing: "%>" (location: (6:11)-(6:13)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (6:13)-(7:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBBlockNode (location: (7:2)-(9:11)) + │ │ │ ├── tag_opening: "<%=" (location: (7:2)-(7:5)) + │ │ │ ├── content: " form_for(obj) do |f| " (location: (7:5)-(7:27)) + │ │ │ ├── tag_closing: "%>" (location: (7:27)-(7:29)) + │ │ │ ├── body: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (7:29)-(9:2)) + │ │ │ │ └── content: "\n Form B\n " + │ │ │ │ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (9:2)-(9:11)) + │ │ │ ├── tag_opening: "<%" (location: (9:2)-(9:4)) + │ │ │ ├── content: " end " (location: (9:4)-(9:9)) + │ │ │ └── tag_closing: "%>" (location: (9:9)-(9:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (9:11)-(10:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (10:0)-(10:9)) + │ ├── tag_opening: "<%" (location: (10:0)-(10:2)) + │ ├── content: " end " (location: (10:2)-(10:7)) + │ └── tag_closing: "%>" (location: (10:7)-(10:9)) + │ + │ + └── @ HTMLTextNode (location: (10:9)-(11:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt b/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt new file mode 100644 index 000000000..5cef31f1d --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt @@ -0,0 +1,49 @@ +@ DocumentNode (location: (1:0)-(7:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case status " (location: (1:2)-(1:15)) + │ ├── tag_closing: "%>" (location: (1:15)-(1:17)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:17)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (2:0)-(2:18)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " when :active " (location: (2:2)-(2:16)) + │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBIfNode (location: (3:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (3:2)-(3:4)) + │ │ │ ├── content: " if admin? " (location: (3:4)-(3:15)) + │ │ │ ├── tag_closing: "%>" (location: (3:15)-(3:17)) + │ │ │ ├── statements: (1 item) + │ │ │ │ └── @ HTMLTextNode (location: (3:17)-(5:2)) + │ │ │ │ └── content: "\n Admin view\n " + │ │ │ │ + │ │ │ ├── subsequent: ∅ + │ │ │ └── end_node: + │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11)) + │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4)) + │ │ │ ├── content: " end " (location: (5:4)-(5:9)) + │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11)) + │ │ │ + │ │ │ + │ │ └── @ HTMLTextNode (location: (5:11)-(6:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (6:0)-(6:9)) + │ ├── tag_opening: "<%" (location: (6:0)-(6:2)) + │ ├── content: " end " (location: (6:2)-(6:7)) + │ └── tag_closing: "%>" (location: (6:7)-(6:9)) + │ + │ + └── @ HTMLTextNode (location: (6:9)-(7:0)) + └── content: "\n" \ No newline at end of file From 5067d4e595a0829d2c48a7e2fe71817a0d9bf0e3 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 5 Oct 2025 16:58:06 +0900 Subject: [PATCH 17/97] Parser: Analyze `case` statements with `yield` as `ERBCaseNode` (#577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request updates the parser to analyze `yield` inside `case` nodes as `ERBCaseNode` instead of `ERBYieldNode`. The following templates: ```html+erb <% case yield(:a) %> <% when 'a' %> aaa <% end %> ``` Gets now parsed as: ```js @ DocumentNode (location: (1:0)-(5:0)) └── children: (2 items) ├── @ ERBCaseNode (location: (1:0)-(4:9)) │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) │ ├── content: " case yield(:a) " (location: (1:2)-(1:18)) │ ├── tag_closing: "%>" (location: (1:18)-(1:20)) │ ├── children: (1 item) │ │ └── @ HTMLTextNode (location: (1:20)-(2:0)) │ │ └── content: "\n" │ │ │ ├── conditions: (1 item) │ │ └── @ ERBWhenNode (location: (2:0)-(2:14)) │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) │ │ ├── content: " when 'a' " (location: (2:2)-(2:12)) │ │ ├── tag_closing: "%>" (location: (2:12)-(2:14)) │ │ └── statements: (1 item) │ │ └── @ HTMLTextNode (location: (2:14)-(4:0)) │ │ └── content: "\n aaa\n" │ │ │ │ │ ├── else_clause: ∅ │ └── end_node: │ └── @ ERBEndNode (location: (4:0)-(4:9)) │ ├── tag_opening: "<%" (location: (4:0)-(4:2)) │ ├── content: " end " (location: (4:2)-(4:7)) │ └── tag_closing: "%>" (location: (4:7)-(4:9)) │ │ └── @ HTMLTextNode (location: (4:9)-(5:0)) └── content: "\n" ``` Previously it was parsed as: ```js @ DocumentNode (location: (1:0)-(5:0)) └── children: (6 items) ├── @ ERBYieldNode (location: (1:0)-(1:20)) │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) │ ├── content: " case yield(:a) " (location: (1:2)-(1:18)) │ └── tag_closing: "%>" (location: (1:18)-(1:20)) │ ├── @ HTMLTextNode (location: (1:20)-(2:0)) │ └── content: "\n" │ ├── @ ERBContentNode (location: (2:0)-(2:14)) │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) │ ├── content: " when 'a' " (location: (2:2)-(2:12)) │ ├── tag_closing: "%>" (location: (2:12)-(2:14)) │ ├── parsed: true │ └── valid: false │ ├── @ HTMLTextNode (location: (2:14)-(4:0)) │ └── content: "\n aaa\n" │ ├── @ ERBContentNode (location: (4:0)-(4:9)) │ ├── tag_opening: "<%" (location: (4:0)-(4:2)) │ ├── content: " end " (location: (4:2)-(4:7)) │ ├── tag_closing: "%>" (location: (4:7)-(4:9)) │ ├── parsed: true │ └── valid: false │ └── @ HTMLTextNode (location: (4:9)-(5:0)) └── content: "\n" ``` Resolves https://github.com/marcoroth/herb/issues/561 --- src/analyze.c | 7 ++--- test/analyze/case_test.rb | 9 ++++++ ...yield_dc59b65969b8ef95076abc05c3907392.txt | 30 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt diff --git a/src/analyze.c b/src/analyze.c index bd0778520..dcf5e5485 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -96,12 +96,8 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) { if (!ruby) { return CONTROL_TYPE_UNKNOWN; } - if (ruby->valid) { - if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; } - return CONTROL_TYPE_UNKNOWN; - } + if (ruby->valid) { return CONTROL_TYPE_UNKNOWN; } - if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; } if (has_block_node(ruby)) { return CONTROL_TYPE_BLOCK; } if (has_if_node(ruby)) { return CONTROL_TYPE_IF; } if (has_elsif_node(ruby)) { return CONTROL_TYPE_ELSIF; } @@ -119,6 +115,7 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) { if (has_until_node(ruby)) { return CONTROL_TYPE_UNTIL; } if (has_for_node(ruby)) { return CONTROL_TYPE_FOR; } if (has_block_closing(ruby)) { return CONTROL_TYPE_BLOCK_CLOSE; } + if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; } return CONTROL_TYPE_UNKNOWN; } diff --git a/test/analyze/case_test.rb b/test/analyze/case_test.rb index 0e181c0b4..a4b7bf5d4 100644 --- a/test/analyze/case_test.rb +++ b/test/analyze/case_test.rb @@ -154,5 +154,14 @@ class CaseTest < Minitest::Spec <% end %> HTML end + + test "case with yield" do + assert_parsed_snapshot(<<~HTML) + <% case yield(:a) %> + <% when 'a' %> + aaa + <% end %> + HTML + end end end diff --git a/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt b/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt new file mode 100644 index 000000000..1a4e6654e --- /dev/null +++ b/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt @@ -0,0 +1,30 @@ +@ DocumentNode (location: (1:0)-(5:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(4:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case yield(:a) " (location: (1:2)-(1:18)) + │ ├── tag_closing: "%>" (location: (1:18)-(1:20)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:20)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (2:0)-(2:14)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " when 'a' " (location: (2:2)-(2:12)) + │ │ ├── tag_closing: "%>" (location: (2:12)-(2:14)) + │ │ └── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (2:14)-(4:0)) + │ │ └── content: "\n aaa\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (4:0)-(4:9)) + │ ├── tag_opening: "<%" (location: (4:0)-(4:2)) + │ ├── content: " end " (location: (4:2)-(4:7)) + │ └── tag_closing: "%>" (location: (4:7)-(4:9)) + │ + │ + └── @ HTMLTextNode (location: (4:9)-(5:0)) + └── content: "\n" \ No newline at end of file From 2276a01f37f33c37bdd3d6fb9b9cb0573f7a4e02 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 6 Oct 2025 13:05:38 +0900 Subject: [PATCH 18/97] `v0.7.5` --- Gemfile.lock | 2 +- docs/package.json | 6 ++--- javascript/packages/browser/package.json | 4 ++-- .../packages/browser/test/browser.test.ts | 2 +- javascript/packages/core/package.json | 2 +- javascript/packages/dev-tools/package.json | 4 ++-- javascript/packages/formatter/package.json | 6 ++--- .../herb-language-server/package.json | 4 ++-- javascript/packages/highlighter/package.json | 6 ++--- .../packages/language-server/package.json | 8 +++---- javascript/packages/linter/README.md | 4 ++-- javascript/packages/linter/package.json | 10 ++++---- .../test/__snapshots__/cli.test.ts.snap | 24 +++++++++---------- javascript/packages/node-wasm/package.json | 4 ++-- .../packages/node-wasm/test/node-wasm.test.ts | 2 +- javascript/packages/node/package.json | 4 ++-- javascript/packages/node/test/node.test.ts | 2 +- javascript/packages/printer/package.json | 4 ++-- .../packages/stimulus-lint/package.json | 10 ++++---- .../tailwind-class-sorter/package.json | 2 +- javascript/packages/vscode/package.json | 8 +++---- lib/herb/version.rb | 2 +- playground/package.json | 6 ++--- src/include/version.h | 2 +- test/c/test_herb.c | 2 +- test/herb_test.rb | 2 +- 26 files changed, 66 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4e87cabfe..2199da0f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,7 +17,7 @@ GIT PATH remote: . specs: - herb (0.7.4) + herb (0.7.5) GEM remote: https://rubygems.org/ diff --git a/docs/package.json b/docs/package.json index f6685b400..cbda66736 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,9 +19,9 @@ "fetch:contributors": "mkdir -p .vitepress/data/ && gh api -X get https://api.github.com/repos/marcoroth/herb/contributors > .vitepress/data/contributors.json" }, "dependencies": { - "@herb-tools/browser": "0.7.4", - "@herb-tools/core": "0.7.4", - "@herb-tools/node": "0.7.4" + "@herb-tools/browser": "0.7.5", + "@herb-tools/core": "0.7.5", + "@herb-tools/node": "0.7.5" }, "devDependencies": { "@shikijs/vitepress-twoslash": "^3.4.2", diff --git a/javascript/packages/browser/package.json b/javascript/packages/browser/package.json index c65ea539a..55622a4ad 100644 --- a/javascript/packages/browser/package.json +++ b/javascript/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/browser", - "version": "0.7.4", + "version": "0.7.5", "description": "WebAssembly-based HTML-aware ERB parser for browsers.", "type": "module", "license": "MIT", @@ -34,7 +34,7 @@ } }, "dependencies": { - "@herb-tools/core": "0.7.4" + "@herb-tools/core": "0.7.5" }, "files": [ "package.json", diff --git a/javascript/packages/browser/test/browser.test.ts b/javascript/packages/browser/test/browser.test.ts index ff99b6553..464db9da9 100644 --- a/javascript/packages/browser/test/browser.test.ts +++ b/javascript/packages/browser/test/browser.test.ts @@ -17,7 +17,7 @@ describe("@herb-tools/browser", () => { test("version() returns a string", async () => { const version = Herb.version expect(typeof version).toBe("string") - expect(version).toBe("@herb-tools/browser@0.7.4, @herb-tools/core@0.7.4, libprism@1.5.1, libherb@0.7.4 (WebAssembly)") + expect(version).toBe("@herb-tools/browser@0.7.5, @herb-tools/core@0.7.5, libprism@1.5.1, libherb@0.7.5 (WebAssembly)") }) test("parse() can process a simple template", async () => { diff --git a/javascript/packages/core/package.json b/javascript/packages/core/package.json index 109a4d029..6ae07a8a2 100644 --- a/javascript/packages/core/package.json +++ b/javascript/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/core", - "version": "0.7.4", + "version": "0.7.5", "description": "Core module exporting shared interfaces, AST node definitions, and common utilities for Herb", "type": "module", "license": "MIT", diff --git a/javascript/packages/dev-tools/package.json b/javascript/packages/dev-tools/package.json index c66f0ff4e..4dbe1b66f 100644 --- a/javascript/packages/dev-tools/package.json +++ b/javascript/packages/dev-tools/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/dev-tools", - "version": "0.7.4", + "version": "0.7.5", "description": "Development tools for visual debugging in HTML+ERB templates", "type": "module", "license": "MIT", @@ -30,7 +30,7 @@ } }, "dependencies": { - "@herb-tools/core": "0.7.4" + "@herb-tools/core": "0.7.5" }, "files": [ "package.json", diff --git a/javascript/packages/formatter/package.json b/javascript/packages/formatter/package.json index 467e1ac21..4de78cad0 100644 --- a/javascript/packages/formatter/package.json +++ b/javascript/packages/formatter/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/formatter", - "version": "0.7.4", + "version": "0.7.5", "description": "Auto-formatter for HTML+ERB templates with intelligent indentation, line wrapping, and ERB-aware pretty-printing.", "license": "MIT", "homepage": "https://herb-tools.dev", @@ -35,8 +35,8 @@ } }, "dependencies": { - "@herb-tools/core": "0.7.4", - "@herb-tools/printer": "0.7.4" + "@herb-tools/core": "0.7.5", + "@herb-tools/printer": "0.7.5" }, "devDependencies": { "glob": "^11.0.3" diff --git a/javascript/packages/herb-language-server/package.json b/javascript/packages/herb-language-server/package.json index f21a0796f..e7f0e79fb 100644 --- a/javascript/packages/herb-language-server/package.json +++ b/javascript/packages/herb-language-server/package.json @@ -1,7 +1,7 @@ { "name": "herb-language-server", "description": "Placeholder package to reserve the herb-language-server name on NPM; use @herb-tools/language-server instead.", - "version": "0.7.4", + "version": "0.7.5", "author": "Marco Roth", "license": "MIT", "engines": { @@ -45,6 +45,6 @@ "dist/" ], "dependencies": { - "@herb-tools/language-server": "0.7.4" + "@herb-tools/language-server": "0.7.5" } } diff --git a/javascript/packages/highlighter/package.json b/javascript/packages/highlighter/package.json index fa81157f0..4a6a07d1c 100644 --- a/javascript/packages/highlighter/package.json +++ b/javascript/packages/highlighter/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/highlighter", - "version": "0.7.4", + "version": "0.7.5", "description": "Syntax highlighter and diagnostic renderer for HTML+ERB templates.", "license": "MIT", "homepage": "https://herb-tools.dev", @@ -35,8 +35,8 @@ "prepublishOnly": "yarn clean && yarn build && yarn test" }, "dependencies": { - "@herb-tools/core": "0.7.4", - "@herb-tools/node-wasm": "0.7.4", + "@herb-tools/core": "0.7.5", + "@herb-tools/node-wasm": "0.7.5", "glob": "^11.0.3" }, "files": [ diff --git a/javascript/packages/language-server/package.json b/javascript/packages/language-server/package.json index 224cbe31a..a5580abbb 100644 --- a/javascript/packages/language-server/package.json +++ b/javascript/packages/language-server/package.json @@ -1,7 +1,7 @@ { "name": "@herb-tools/language-server", "description": "Herb HTML+ERB Language Tools and Language Server Protocol integration.", - "version": "0.7.4", + "version": "0.7.5", "author": "Marco Roth", "license": "MIT", "engines": { @@ -45,9 +45,9 @@ "dist/" ], "dependencies": { - "@herb-tools/formatter": "0.7.4", - "@herb-tools/linter": "0.7.4", - "@herb-tools/node-wasm": "0.7.4", + "@herb-tools/formatter": "0.7.5", + "@herb-tools/linter": "0.7.5", + "@herb-tools/node-wasm": "0.7.5", "dedent": "^1.6.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" diff --git a/javascript/packages/linter/README.md b/javascript/packages/linter/README.md index f27c4c26d..54002f00f 100644 --- a/javascript/packages/linter/README.md +++ b/javascript/packages/linter/README.md @@ -184,7 +184,7 @@ npx @herb-tools/linter --format=simple --github **Example: `--github` (GitHub annotations + detailed format)** ``` -::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A +::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.5::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A [error] Missing required `alt` attribute on `` tag [html-img-require-alt] @@ -199,7 +199,7 @@ template.html.erb:3:3 **Example: `--format=simple --github` (GitHub annotations + simple format)** ``` -::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A +::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.5::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A template.html.erb: 3:3 ✗ Missing required `alt` attribute on `` tag [html-img-require-alt] diff --git a/javascript/packages/linter/package.json b/javascript/packages/linter/package.json index aa3766a66..e0f6c34e3 100644 --- a/javascript/packages/linter/package.json +++ b/javascript/packages/linter/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/linter", - "version": "0.7.4", + "version": "0.7.5", "description": "HTML+ERB linter for validating HTML structure and enforcing best practices", "license": "MIT", "homepage": "https://herb-tools.dev", @@ -39,10 +39,10 @@ } }, "dependencies": { - "@herb-tools/core": "0.7.4", - "@herb-tools/highlighter": "0.7.4", - "@herb-tools/node-wasm": "0.7.4", - "@herb-tools/printer": "0.7.4", + "@herb-tools/core": "0.7.5", + "@herb-tools/highlighter": "0.7.5", + "@herb-tools/node-wasm": "0.7.5", + "@herb-tools/printer": "0.7.5", "glob": "^11.0.3" }, "files": [ diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index d24c9fd66..8ba66e99b 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`CLI Output Formatting > GitHub Actions format escapes special characters in messages 1`] = ` -"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A +"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.5::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A [error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt) @@ -55,7 +55,7 @@ test/fixtures/test-file-with-errors.html.erb:2:22 `; exports[`CLI Output Formatting > GitHub Actions format includes rule codes 1`] = ` -"::error file=test/fixtures/no-trailing-newline.html.erb,line=1,col=29,title=erb-requires-trailing-newline • @herb-tools/linter@0.7.4::File must end with trailing newline [erb-requires-trailing-newline]%0A%0A%0Atest/fixtures/no-trailing-newline.html.erb:1:29%0A%0A → 1 │
    No trailing newline
    %0A │ ~%0A +"::error file=test/fixtures/no-trailing-newline.html.erb,line=1,col=29,title=erb-requires-trailing-newline • @herb-tools/linter@0.7.5::File must end with trailing newline [erb-requires-trailing-newline]%0A%0A%0Atest/fixtures/no-trailing-newline.html.erb:1:29%0A%0A → 1 │
    No trailing newline
    %0A │ ~%0A [error] File must end with trailing newline (erb-requires-trailing-newline) @@ -389,9 +389,9 @@ test/fixtures/few-rule-offenses.html.erb:3:16 `; exports[`CLI Output Formatting > formats GitHub Actions output correctly for bad file 1`] = ` -"::error file=test/fixtures/bad-file.html.erb,line=1,col=1,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:1%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A +"::error file=test/fixtures/bad-file.html.erb,line=1,col=1,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:1%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A -::error file=test/fixtures/bad-file.html.erb,line=1,col=16,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:16%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A +::error file=test/fixtures/bad-file.html.erb,line=1,col=16,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:16%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A [error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) @@ -431,11 +431,11 @@ exports[`CLI Output Formatting > formats GitHub Actions output correctly for cle `; exports[`CLI Output Formatting > formats GitHub Actions output correctly for file with errors 1`] = ` -"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A +"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.5::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A [error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt) @@ -782,11 +782,11 @@ test/fixtures/bad-file.html.erb:1:16 `; exports[`CLI Output Formatting > uses GitHub Actions format by default when GITHUB_ACTIONS is true 1`] = ` -"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A +"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.5::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
    %0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
    %0A 5 │%0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A -::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A +::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.5::Closing tag name \`
    \` should be lowercase. Use \`
    \` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
    %0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
    %0A [error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt) diff --git a/javascript/packages/node-wasm/package.json b/javascript/packages/node-wasm/package.json index ae23a1a7e..da589ec3c 100644 --- a/javascript/packages/node-wasm/package.json +++ b/javascript/packages/node-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/node-wasm", - "version": "0.7.4", + "version": "0.7.5", "description": "WebAssembly-based HTML-aware ERB parser for Node.js.", "type": "module", "license": "MIT", @@ -36,7 +36,7 @@ } }, "dependencies": { - "@herb-tools/core": "0.7.4" + "@herb-tools/core": "0.7.5" }, "files": [ "package.json", diff --git a/javascript/packages/node-wasm/test/node-wasm.test.ts b/javascript/packages/node-wasm/test/node-wasm.test.ts index 7533c7b1a..562da6ea2 100644 --- a/javascript/packages/node-wasm/test/node-wasm.test.ts +++ b/javascript/packages/node-wasm/test/node-wasm.test.ts @@ -17,7 +17,7 @@ describe("@herb-tools/node-wasm", () => { test("version() returns a string", async () => { const version = Herb.version expect(typeof version).toBe("string") - expect(version).toBe("@herb-tools/node-wasm@0.7.4, @herb-tools/core@0.7.4, libprism@1.5.1, libherb@0.7.4 (WebAssembly)") + expect(version).toBe("@herb-tools/node-wasm@0.7.5, @herb-tools/core@0.7.5, libprism@1.5.1, libherb@0.7.5 (WebAssembly)") }) test("parse() can process a simple template", async () => { diff --git a/javascript/packages/node/package.json b/javascript/packages/node/package.json index f5f9e9b95..b31eae271 100644 --- a/javascript/packages/node/package.json +++ b/javascript/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/node", - "version": "0.7.4", + "version": "0.7.5", "description": "Native Node.js addon for HTML-aware ERB parsing using Herb.", "type": "module", "license": "MIT", @@ -48,7 +48,7 @@ "host": "https://github.com/marcoroth/herb/releases/download/" }, "dependencies": { - "@herb-tools/core": "0.7.4", + "@herb-tools/core": "0.7.5", "@mapbox/node-pre-gyp": "^2.0.0", "node-addon-api": "^5.1.0" }, diff --git a/javascript/packages/node/test/node.test.ts b/javascript/packages/node/test/node.test.ts index 032656916..8ac737608 100644 --- a/javascript/packages/node/test/node.test.ts +++ b/javascript/packages/node/test/node.test.ts @@ -17,7 +17,7 @@ describe("@herb-tools/node", () => { test("version() returns a string", async () => { const version = Herb.version expect(typeof version).toBe("string") - expect(version).toBe("@herb-tools/node@0.7.4, @herb-tools/core@0.7.4, libprism@1.5.1, libherb@0.7.4 (Node.js C++ native extension)") + expect(version).toBe("@herb-tools/node@0.7.5, @herb-tools/core@0.7.5, libprism@1.5.1, libherb@0.7.5 (Node.js C++ native extension)") }) test("parse() can process a simple template", async () => { diff --git a/javascript/packages/printer/package.json b/javascript/packages/printer/package.json index 16928c514..08de93261 100644 --- a/javascript/packages/printer/package.json +++ b/javascript/packages/printer/package.json @@ -1,6 +1,6 @@ { "name": "@herb-tools/printer", - "version": "0.7.4", + "version": "0.7.5", "description": "AST printer infrastructure and lossless reconstruction tool for HTML+ERB templates", "license": "MIT", "homepage": "https://herb-tools.dev", @@ -37,7 +37,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn test" }, "dependencies": { - "@herb-tools/core": "0.7.4", + "@herb-tools/core": "0.7.5", "glob": "^10.3.10" }, "files": [ diff --git a/javascript/packages/stimulus-lint/package.json b/javascript/packages/stimulus-lint/package.json index f6f62f9b5..3297934fc 100644 --- a/javascript/packages/stimulus-lint/package.json +++ b/javascript/packages/stimulus-lint/package.json @@ -1,6 +1,6 @@ { "name": "stimulus-lint", - "version": "0.1.4", + "version": "0.1.5", "description": "Linting rules for Stimulus controllers and HTML+ERB view templates.", "license": "MIT", "homepage": "https://herb-tools.dev", @@ -34,10 +34,10 @@ "prepublishOnly": "yarn clean && yarn build && yarn test" }, "dependencies": { - "@herb-tools/linter": "0.7.4", - "@herb-tools/core": "0.7.4", - "@herb-tools/node-wasm": "0.7.4", - "@herb-tools/highlighter": "0.7.4", + "@herb-tools/linter": "0.7.5", + "@herb-tools/core": "0.7.5", + "@herb-tools/node-wasm": "0.7.5", + "@herb-tools/highlighter": "0.7.5", "stimulus-parser": "^0.3.0" }, "files": [ diff --git a/javascript/packages/tailwind-class-sorter/package.json b/javascript/packages/tailwind-class-sorter/package.json index 6b6d6eb0d..901efe796 100644 --- a/javascript/packages/tailwind-class-sorter/package.json +++ b/javascript/packages/tailwind-class-sorter/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@herb-tools/tailwind-class-sorter", "description": "Standalone Tailwind CSS class sorter with Prettier plugin compatibility, extracted from tailwindlabs/prettier-plugin-tailwindcss", - "version": "0.7.4", + "version": "0.7.5", "license": "MIT", "main": "./dist/tailwind-class-sorter.cjs", "module": "./dist/tailwind-class-sorter.esm.js", diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json index 63036b345..5b2395279 100644 --- a/javascript/packages/vscode/package.json +++ b/javascript/packages/vscode/package.json @@ -2,7 +2,7 @@ "name": "herb-lsp", "displayName": "Herb LSP - HTML+ERB Language Tools", "description": "VS Code extension for connecting with the Herb Language Server and Language Tools for HTML+ERB files", - "version": "0.7.4", + "version": "0.7.5", "private": true, "license": "MIT", "pricing": "Free", @@ -241,9 +241,9 @@ "prepublishOnly": "yarn clean && yarn build && yarn test" }, "devDependencies": { - "@herb-tools/formatter": "0.7.4", - "@herb-tools/linter": "0.7.4", - "@herb-tools/node-wasm": "0.7.4", + "@herb-tools/formatter": "0.7.5", + "@herb-tools/linter": "0.7.5", + "@herb-tools/node-wasm": "0.7.5", "@types/node": "20.x", "@types/vscode": "^1.43.0", "@typescript-eslint/eslint-plugin": "^8.40.0", diff --git a/lib/herb/version.rb b/lib/herb/version.rb index 47f0c2796..b3a0aa4e9 100644 --- a/lib/herb/version.rb +++ b/lib/herb/version.rb @@ -2,5 +2,5 @@ # typed: true module Herb - VERSION = "0.7.4" + VERSION = "0.7.5" end diff --git a/playground/package.json b/playground/package.json index 31c6c5407..37c24a366 100644 --- a/playground/package.json +++ b/playground/package.json @@ -19,9 +19,9 @@ }, "dependencies": { "@alenaksu/json-viewer": "^2.0.1", - "@herb-tools/browser": "0.7.4", - "@herb-tools/formatter": "0.7.4", - "@herb-tools/linter": "0.7.4", + "@herb-tools/browser": "0.7.5", + "@herb-tools/formatter": "0.7.5", + "@herb-tools/linter": "0.7.5", "@hotwired/stimulus": "^3.2.2", "dedent": "^1.6.0", "express": "^5.1.0", diff --git a/src/include/version.h b/src/include/version.h index 4185a598a..a75f8cba0 100644 --- a/src/include/version.h +++ b/src/include/version.h @@ -1,6 +1,6 @@ #ifndef HERB_VERSION_H #define HERB_VERSION_H -#define HERB_VERSION "0.7.4" +#define HERB_VERSION "0.7.5" #endif diff --git a/test/c/test_herb.c b/test/c/test_herb.c index 7eab745f0..bc0c1272c 100644 --- a/test/c/test_herb.c +++ b/test/c/test_herb.c @@ -2,7 +2,7 @@ #include "../../src/include/herb.h" TEST(test_herb_version) - ck_assert_str_eq(herb_version(), "0.7.4"); + ck_assert_str_eq(herb_version(), "0.7.5"); END TCase *herb_tests(void) { diff --git a/test/herb_test.rb b/test/herb_test.rb index 866423e31..c32f572d0 100644 --- a/test/herb_test.rb +++ b/test/herb_test.rb @@ -4,6 +4,6 @@ class HerbTest < Minitest::Spec test "version" do - assert_equal "herb gem v0.7.4, libprism v1.5.1, libherb v0.7.4 (Ruby C native extension)", Herb.version + assert_equal "herb gem v0.7.5, libprism v1.5.1, libherb v0.7.5 (Ruby C native extension)", Herb.version end end From b4a8e359db4779e5b1da95b203bd1766fa213b99 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 6 Oct 2025 14:42:29 +0900 Subject: [PATCH 19/97] Update `bin/setup` script --- bin/setup | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/setup b/bin/setup index 361c7f2e6..bb27d1eb8 100755 --- a/bin/setup +++ b/bin/setup @@ -2,5 +2,7 @@ set -e # Exit on error -make prism -make all +bundle install +bundle exec rake templates +yarn install +yarn build From caab4f14f6307e087e8f5fe58876dca4d2664e21 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 6 Oct 2025 14:42:41 +0900 Subject: [PATCH 20/97] Add `bin/publish_packages` script --- bin/publish_packages | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 bin/publish_packages diff --git a/bin/publish_packages b/bin/publish_packages new file mode 100755 index 000000000..10766ab81 --- /dev/null +++ b/bin/publish_packages @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +echo "Building all packages..." +yarn nx run-many -t build --all + +echo "Running tests in all packages..." +yarn nx run-many -t test --all + +for package_dir in javascript/packages/*; do + if [ -d "$package_dir" ] && [ -f "$package_dir/package.json" ]; then + package=$(basename "$package_dir") + + if [ "$package" = "vscode" ]; then + echo "Skipping vscode package..." + continue + fi + + echo "Publishing $package..." + (cd "$package_dir" && yarn publish) + fi +done + +echo "All packages published successfully!" From 1af6962dc1039d173e753c962f2ce949a9896928 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 8 Oct 2025 10:02:48 +0200 Subject: [PATCH 21/97] C: Also call `analyze` in C-CLI `parse` command (#584) This pull request updates the C-CLI to also call `herb_analyze_parse_tree` in the `parse` command, so that the `ERBContentNodes` also get analyzed in a HTML+ERB document. AS discussed in https://github.com/marcoroth/herb/issues/406#issuecomment-3350166452 --- src/main.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.c b/src/main.c index d89af37b0..9ce19649a 100644 --- a/src/main.c +++ b/src/main.c @@ -106,6 +106,9 @@ int main(const int argc, char* argv[]) { if (strcmp(argv[1], "parse") == 0) { AST_DOCUMENT_NODE_T* root = herb_parse(source, NULL); + + herb_analyze_parse_tree(root, source); + clock_gettime(CLOCK_MONOTONIC, &end); ast_pretty_print_node((AST_NODE_T*) root, 0, 0, &output); From 1f3ac1d5416120a52bc12f9f2b0ed36d5296d23a Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 8 Oct 2025 12:10:35 +0200 Subject: [PATCH 22/97] C: Favor explicit buffer capacity over default capacity (#585) This pull request is an alternative to #579 and reworks the buffer to not have a default capacity anymore, but instead, let the caller decide how big the initial buffer capacity should be. This also allows callers to request enough capacity upfront if they know the approximate or exact buffer buffer length, which then doesn't need any buffer capacity expansions at a later point, thus removing the need to reallocate. Closes #579 --- bin/leaks_parse | 13 ++- ext/herb/extension.c | 12 +-- javascript/packages/node/extension/herb.cpp | 12 +-- src/analyze.c | 2 + src/buffer.c | 25 +++--- src/extract.c | 9 +- src/herb.c | 7 +- src/include/buffer.h | 6 +- src/io.c | 5 +- src/lexer.c | 15 ++-- src/main.c | 25 +++--- src/parser.c | 64 +++++++------- src/token.c | 18 ++-- test/c/test_buffer.c | 82 +++++++++++------- test/c/test_json.c | 93 +++++++++++++-------- test/c/test_lex.c | 10 ++- test/c/test_token.c | 10 ++- wasm/herb-wasm.cpp | 10 ++- 18 files changed, 257 insertions(+), 161 deletions(-) diff --git a/bin/leaks_parse b/bin/leaks_parse index 603132e80..0212a2948 100755 --- a/bin/leaks_parse +++ b/bin/leaks_parse @@ -1,7 +1,7 @@ #!/bin/bash if [ -z "$1" ]; then - echo "Usage: $0 " + echo "Usage: $0 " exit 1 fi @@ -10,4 +10,13 @@ if [[ "$(uname)" != "Darwin" ]]; then exit 0 fi -leaks --atExit -- ./herb parse "$1" +TARGET="$1" + +if [ -d "$TARGET" ]; then + while IFS= read -r -d '' file; do + echo "Checking $file for leaks..." + leaks --atExit -- ./herb parse "$file" --silent || exit 1 + done < <(find "$TARGET" -name "*.html.erb" -type f -print0) +else + leaks --atExit -- ./herb parse "$TARGET" --silent +fi diff --git a/ext/herb/extension.c b/ext/herb/extension.c index 7f83a0090..9b2884aee 100644 --- a/ext/herb/extension.c +++ b/ext/herb/extension.c @@ -89,13 +89,13 @@ static VALUE Herb_lex_to_json(VALUE self, VALUE source) { char* string = (char*) check_string(source); buffer_T output; - if (!buffer_init(&output)) { return Qnil; } + if (!buffer_init(&output, 4096)) { return Qnil; } herb_lex_json_to_buffer(string, &output); VALUE result = rb_str_new(output.value, output.length); - buffer_free(&output); + free(output.value); return result; } @@ -104,12 +104,12 @@ static VALUE Herb_extract_ruby(VALUE self, VALUE source) { char* string = (char*) check_string(source); buffer_T output; - if (!buffer_init(&output)) { return Qnil; } + if (!buffer_init(&output, strlen(string))) { return Qnil; } herb_extract_ruby_to_buffer(string, &output); VALUE result = rb_utf8_str_new_cstr(output.value); - buffer_free(&output); + free(output.value); return result; } @@ -118,12 +118,12 @@ static VALUE Herb_extract_html(VALUE self, VALUE source) { char* string = (char*) check_string(source); buffer_T output; - if (!buffer_init(&output)) { return Qnil; } + if (!buffer_init(&output, strlen(string))) { return Qnil; } herb_extract_html_to_buffer(string, &output); VALUE result = rb_utf8_str_new_cstr(output.value); - buffer_free(&output); + free(output.value); return result; } diff --git a/javascript/packages/node/extension/herb.cpp b/javascript/packages/node/extension/herb.cpp index 3b8ba8ebb..a3e080902 100644 --- a/javascript/packages/node/extension/herb.cpp +++ b/javascript/packages/node/extension/herb.cpp @@ -156,7 +156,7 @@ napi_value Herb_lex_to_json(napi_env env, napi_callback_info info) { if (!string) { return nullptr; } buffer_T output; - if (!buffer_init(&output)) { + if (!buffer_init(&output, 4096)) { free(string); napi_throw_error(env, nullptr, "Failed to initialize buffer"); return nullptr; @@ -167,7 +167,7 @@ napi_value Herb_lex_to_json(napi_env env, napi_callback_info info) { napi_value result; napi_create_string_utf8(env, output.value, output.length, &result); - buffer_free(&output); + free(output.value); free(string); return result; @@ -187,7 +187,7 @@ napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) { if (!string) { return nullptr; } buffer_T output; - if (!buffer_init(&output)) { + if (!buffer_init(&output, strlen(string))) { free(string); napi_throw_error(env, nullptr, "Failed to initialize buffer"); return nullptr; @@ -198,7 +198,7 @@ napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) { napi_value result; napi_create_string_utf8(env, output.value, NAPI_AUTO_LENGTH, &result); - buffer_free(&output); + free(output.value); free(string); return result; } @@ -217,7 +217,7 @@ napi_value Herb_extract_html(napi_env env, napi_callback_info info) { if (!string) { return nullptr; } buffer_T output; - if (!buffer_init(&output)) { + if (!buffer_init(&output, strlen(string))) { free(string); napi_throw_error(env, nullptr, "Failed to initialize buffer"); return nullptr; @@ -228,7 +228,7 @@ napi_value Herb_extract_html(napi_env env, napi_callback_info info) { napi_value result; napi_create_string_utf8(env, output.value, NAPI_AUTO_LENGTH, &result); - buffer_free(&output); + free(output.value); free(string); return result; } diff --git a/src/analyze.c b/src/analyze.c index dcf5e5485..ca8ada004 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -1069,6 +1069,8 @@ void herb_analyze_parse_tree(AST_DOCUMENT_NODE_T* document, const char* source) void herb_analyze_parse_errors(AST_DOCUMENT_NODE_T* document, const char* source) { char* extracted_ruby = herb_extract_ruby_with_semicolons(source); + if (!extracted_ruby) { return; } + pm_parser_t parser; pm_options_t options = { 0, .partial_script = true }; pm_parser_init(&parser, (const uint8_t*) extracted_ruby, strlen(extracted_ruby), &options); diff --git a/src/buffer.c b/src/buffer.c index f162c4116..adc08292e 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -7,8 +7,8 @@ #include "include/memory.h" #include "include/util.h" -bool buffer_init(buffer_T* buffer) { - buffer->capacity = 1024; +bool buffer_init(buffer_T* buffer, const size_t capacity) { + buffer->capacity = capacity; buffer->length = 0; buffer->value = nullable_safe_malloc((buffer->capacity + 1) * sizeof(char)); @@ -22,9 +22,14 @@ bool buffer_init(buffer_T* buffer) { return true; } -buffer_T buffer_new(void) { - buffer_T buffer; - buffer_init(&buffer); +buffer_T* buffer_new(const size_t capacity) { + buffer_T* buffer = safe_malloc(sizeof(buffer_T)); + + if (!buffer_init(buffer, capacity)) { + free(buffer); + return NULL; + } + return buffer; } @@ -231,11 +236,11 @@ void buffer_clear(buffer_T* buffer) { buffer->value[0] = '\0'; } -void buffer_free(buffer_T* buffer) { - if (!buffer) { return; } +void buffer_free(buffer_T** buffer) { + if (!buffer || !*buffer) { return; } - if (buffer->value != NULL) { free(buffer->value); } + if ((*buffer)->value != NULL) { free((*buffer)->value); } - buffer->value = NULL; - buffer->length = buffer->capacity = 0; + free(*buffer); + *buffer = NULL; } diff --git a/src/extract.c b/src/extract.c index cc7bedb49..ea3fdb93d 100644 --- a/src/extract.c +++ b/src/extract.c @@ -5,6 +5,7 @@ #include "include/lexer.h" #include +#include void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* output) { array_T* tokens = herb_lex(source); @@ -121,8 +122,10 @@ void herb_extract_html_to_buffer(const char* source, buffer_T* output) { } char* herb_extract_ruby_with_semicolons(const char* source) { + if (!source) { return NULL; } + buffer_T output; - buffer_init(&output); + buffer_init(&output, strlen(source)); herb_extract_ruby_to_buffer_with_semicolons(source, &output); @@ -130,8 +133,10 @@ char* herb_extract_ruby_with_semicolons(const char* source) { } char* herb_extract(const char* source, const herb_extract_language_T language) { + if (!source) { return NULL; } + buffer_T output; - buffer_init(&output); + buffer_init(&output, strlen(source)); switch (language) { case HERB_EXTRACT_LANGUAGE_RUBY: herb_extract_ruby_to_buffer(source, &output); break; diff --git a/src/herb.c b/src/herb.c index d057b28a8..1348c9495 100644 --- a/src/herb.c +++ b/src/herb.c @@ -28,6 +28,8 @@ array_T* herb_lex(const char* source) { } AST_DOCUMENT_NODE_T* herb_parse(const char* source, parser_options_T* options) { + if (!source) { source = ""; } + lexer_T* lexer = lexer_init(source); parser_T* parser = herb_parser_init(lexer, options); @@ -66,7 +68,8 @@ void herb_lex_to_buffer(const char* source, buffer_T* output) { void herb_lex_json_to_buffer(const char* source, buffer_T* output) { array_T* tokens = herb_lex(source); - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 4096); json_start_root_array(&json); for (size_t i = 0; i < array_size(tokens); i++) { @@ -79,7 +82,7 @@ void herb_lex_json_to_buffer(const char* source, buffer_T* output) { json_end_array(&json); buffer_concat(output, &json); - buffer_free(&json); + free(json.value); herb_free_tokens(&tokens); } diff --git a/src/include/buffer.h b/src/include/buffer.h index a684e9ded..04c7fcb2b 100644 --- a/src/include/buffer.h +++ b/src/include/buffer.h @@ -10,8 +10,8 @@ typedef struct BUFFER_STRUCT { size_t capacity; } buffer_T; -bool buffer_init(buffer_T* buffer); -buffer_T buffer_new(void); +bool buffer_init(buffer_T* buffer, size_t capacity); +buffer_T* buffer_new(size_t capacity); bool buffer_increase_capacity(buffer_T* buffer, size_t additional_capacity); bool buffer_has_capacity(buffer_T* buffer, size_t required_length); @@ -34,6 +34,6 @@ size_t buffer_capacity(const buffer_T* buffer); size_t buffer_sizeof(void); void buffer_clear(buffer_T* buffer); -void buffer_free(buffer_T* buffer); +void buffer_free(buffer_T** buffer); #endif diff --git a/src/io.c b/src/io.c index a1ef45632..9ee5b9fe0 100644 --- a/src/io.c +++ b/src/io.c @@ -8,6 +8,8 @@ #define FILE_READ_CHUNK 4096 char* herb_read_file(const char* filename) { + if (!filename) { return NULL; } + FILE* fp = fopen(filename, "rb"); if (fp == NULL) { @@ -15,7 +17,8 @@ char* herb_read_file(const char* filename) { exit(1); } - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 4096); char chunk[FILE_READ_CHUNK]; size_t bytes_read; diff --git a/src/lexer.c b/src/lexer.c index f62bbd696..1eaac649a 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -174,7 +174,8 @@ static token_T* lexer_match_and_advance(lexer_T* lexer, const char* value, const // ===== Specialized Parsers static token_T* lexer_parse_whitespace(lexer_T* lexer) { - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 128); while (isspace(lexer->current_character) && lexer->current_character != '\n' && lexer->current_character != '\r' && !lexer_eof(lexer)) { @@ -184,13 +185,14 @@ static token_T* lexer_parse_whitespace(lexer_T* lexer) { token_T* token = token_init(buffer.value, TOKEN_WHITESPACE, lexer); - buffer_free(&buffer); + free(buffer.value); return token; } static token_T* lexer_parse_identifier(lexer_T* lexer) { - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 128); while ((isalnum(lexer->current_character) || lexer->current_character == '-' || lexer->current_character == '_' || lexer->current_character == ':') @@ -202,7 +204,7 @@ static token_T* lexer_parse_identifier(lexer_T* lexer) { token_T* token = token_init(buffer.value, TOKEN_IDENTIFIER, lexer); - buffer_free(&buffer); + free(buffer.value); return token; } @@ -223,7 +225,8 @@ static token_T* lexer_parse_erb_open(lexer_T* lexer) { } static token_T* lexer_parse_erb_content(lexer_T* lexer) { - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); while (!lexer_peek_erb_end(lexer, 0)) { if (lexer_eof(lexer)) { @@ -247,7 +250,7 @@ static token_T* lexer_parse_erb_content(lexer_T* lexer) { token_T* token = token_init(buffer.value, TOKEN_ERB_CONTENT, lexer); - buffer_free(&buffer); + free(buffer.value); return token; } diff --git a/src/main.c b/src/main.c index 9ce19649a..2e1efbfa2 100644 --- a/src/main.c +++ b/src/main.c @@ -55,7 +55,7 @@ int main(const int argc, char* argv[]) { buffer_T output; - if (!buffer_init(&output)) { return 1; } + if (!buffer_init(&output, 4096)) { return 1; } char* source = herb_read_file(argv[2]); @@ -74,7 +74,7 @@ int main(const int argc, char* argv[]) { print_time_diff(start, end, "visiting"); ast_node_free((AST_NODE_T*) root); - buffer_free(&output); + free(output.value); free(source); return 0; @@ -87,7 +87,7 @@ int main(const int argc, char* argv[]) { printf("%s\n", output.value); print_time_diff(start, end, "lexing"); - buffer_free(&output); + free(output.value); free(source); return 0; @@ -98,7 +98,7 @@ int main(const int argc, char* argv[]) { printf("%s\n", output.value); - buffer_free(&output); + free(output.value); free(source); return 0; @@ -111,13 +111,18 @@ int main(const int argc, char* argv[]) { clock_gettime(CLOCK_MONOTONIC, &end); - ast_pretty_print_node((AST_NODE_T*) root, 0, 0, &output); - printf("%s\n", output.value); + int silent = 0; + if (argc > 3 && strcmp(argv[3], "--silent") == 0) { silent = 1; } + + if (!silent) { + ast_pretty_print_node((AST_NODE_T*) root, 0, 0, &output); + printf("%s\n", output.value); - print_time_diff(start, end, "parsing"); + print_time_diff(start, end, "parsing"); + } ast_node_free((AST_NODE_T*) root); - buffer_free(&output); + free(output.value); free(source); return 0; @@ -130,7 +135,7 @@ int main(const int argc, char* argv[]) { printf("%s\n", output.value); print_time_diff(start, end, "extracting Ruby"); - buffer_free(&output); + free(output.value); free(source); return 0; @@ -143,7 +148,7 @@ int main(const int argc, char* argv[]) { printf("%s\n", output.value); print_time_diff(start, end, "extracting HTML"); - buffer_free(&output); + free(output.value); free(source); return 0; diff --git a/src/parser.c b/src/parser.c index 47847d9b5..9877f2538 100644 --- a/src/parser.c +++ b/src/parser.c @@ -53,7 +53,8 @@ parser_T* herb_parser_init(lexer_T* lexer, parser_options_T* options) { static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) { array_T* errors = array_init(8); array_T* children = array_init(8); - buffer_T content = buffer_new(); + buffer_T content; + buffer_init(&content, 128); token_T* tag_opening = parser_consume_expected(parser, TOKEN_CDATA_START, errors); position_T start = parser->current_token->location.start; @@ -84,7 +85,7 @@ static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) { errors ); - buffer_free(&content); + free(content.value); token_free(tag_opening); token_free(tag_closing); @@ -97,7 +98,8 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { token_T* comment_start = parser_consume_expected(parser, TOKEN_HTML_COMMENT_START, errors); position_T start = parser->current_token->location.start; - buffer_T comment = buffer_new(); + buffer_T comment; + buffer_init(&comment, 512); while (token_is_none_of(parser, TOKEN_HTML_COMMENT_END, TOKEN_EOF)) { if (token_is(parser, TOKEN_ERB_START)) { @@ -129,7 +131,7 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { errors ); - buffer_free(&comment); + free(comment.value); token_free(comment_start); token_free(comment_end); @@ -139,7 +141,8 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) { static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) { array_T* errors = array_init(8); array_T* children = array_init(8); - buffer_T content = buffer_new(); + buffer_T content; + buffer_init(&content, 64); token_T* tag_opening = parser_consume_expected(parser, TOKEN_HTML_DOCTYPE, errors); @@ -175,7 +178,7 @@ static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) { token_free(tag_opening); token_free(tag_closing); - buffer_free(&content); + free(content.value); return doctype; } @@ -183,7 +186,8 @@ static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) { static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser) { array_T* errors = array_init(8); array_T* children = array_init(8); - buffer_T content = buffer_new(); + buffer_T content; + buffer_init(&content, 64); token_T* tag_opening = parser_consume_expected(parser, TOKEN_XML_DECLARATION, errors); @@ -221,7 +225,7 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser token_free(tag_opening); token_free(tag_closing); - buffer_free(&content); + free(content.value); return xml_declaration; } @@ -229,7 +233,8 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T* document_errors) { position_T start = parser->current_token->location.start; - buffer_T content = buffer_new(); + buffer_T content; + buffer_init(&content, 2048); while (token_is_none_of( parser, @@ -241,7 +246,7 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T TOKEN_EOF )) { if (token_is(parser, TOKEN_ERROR)) { - buffer_free(&content); + free(content.value); token_T* token = parser_consume_expected(parser, TOKEN_ERROR, document_errors); append_unexpected_error( @@ -265,18 +270,15 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T array_T* errors = array_init(8); - if (buffer_length(&content) > 0) { - AST_HTML_TEXT_NODE_T* text_node = - ast_html_text_node_init(buffer_value(&content), start, parser->current_token->location.start, errors); - - buffer_free(&content); + AST_HTML_TEXT_NODE_T* text_node = NULL; - return text_node; + if (buffer_length(&content) > 0) { + text_node = ast_html_text_node_init(buffer_value(&content), start, parser->current_token->location.start, errors); + } else { + text_node = ast_html_text_node_init("", start, parser->current_token->location.start, errors); } - AST_HTML_TEXT_NODE_T* text_node = ast_html_text_node_init("", start, parser->current_token->location.start, errors); - - buffer_free(&content); + free(content.value); return text_node; } @@ -284,7 +286,8 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T* parser) { array_T* errors = array_init(8); array_T* children = array_init(8); - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 128); position_T start = parser->current_token->location.start; while (token_is_none_of( @@ -330,7 +333,7 @@ static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T AST_HTML_ATTRIBUTE_NAME_NODE_T* attribute_name = ast_html_attribute_name_node_init(children, node_start, node_end, errors); - buffer_free(&buffer); + free(buffer.value); return attribute_name; } @@ -340,7 +343,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value array_T* children, array_T* errors ) { - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 512); token_T* opening_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors); position_T start = parser->current_token->location.start; @@ -442,7 +446,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value } parser_append_literal_node_from_buffer(parser, &buffer, children, start); - buffer_free(&buffer); + free(buffer.value); token_T* closing_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors); @@ -566,7 +570,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) || lexer_peek_for_token_type_after_whitespace(parser->lexer, TOKEN_EQUALS); if (has_equals) { - buffer_T equals_buffer = buffer_new(); + buffer_T equals_buffer; + buffer_init(&equals_buffer, 256); position_T equals_start = { 0 }; position_T equals_end = { 0 }; uint32_t range_start = 0; @@ -613,7 +618,7 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) equals_with_whitespace->location = (location_T) { .start = equals_start, .end = equals_end }; equals_with_whitespace->range = (range_T) { .from = range_start, .to = range_end }; - buffer_free(&equals_buffer); + free(equals_buffer.value); AST_HTML_ATTRIBUTE_VALUE_NODE_T* attribute_value = parser_parse_html_attribute_value(parser); @@ -1018,13 +1023,14 @@ static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) { } static void parser_parse_foreign_content(parser_T* parser, array_T* children, array_T* errors) { - buffer_T content = buffer_new(); + buffer_T content; + buffer_init(&content, 1024); position_T start = parser->current_token->location.start; const char* expected_closing_tag = parser_get_foreign_content_closing_tag(parser->foreign_content_type); if (expected_closing_tag == NULL) { parser_exit_foreign_content(parser); - buffer_free(&content); + free(content.value); return; } @@ -1059,7 +1065,7 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar parser_append_literal_node_from_buffer(parser, &content, children, start); parser_exit_foreign_content(parser); - buffer_free(&content); + free(content.value); return; } @@ -1072,7 +1078,7 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar parser_append_literal_node_from_buffer(parser, &content, children, start); parser_exit_foreign_content(parser); - buffer_free(&content); + free(content.value); } static void parser_parse_in_data_state(parser_T* parser, array_T* children, array_T* errors) { diff --git a/src/token.c b/src/token.c index 37c7d4d89..60a5010ac 100644 --- a/src/token.c +++ b/src/token.c @@ -120,34 +120,38 @@ char* token_to_string(const token_T* token) { } char* token_to_json(const token_T* token) { - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 512); json_start_root_object(&json); json_add_string(&json, "type", token_type_to_string(token->type)); json_add_string(&json, "value", token->value); - buffer_T range = buffer_new(); + buffer_T range; + buffer_init(&range, 128); json_start_array(&json, "range"); json_add_size_t(&range, NULL, token->range.from); json_add_size_t(&range, NULL, token->range.to); buffer_concat(&json, &range); - buffer_free(&range); + free(range.value); json_end_array(&json); - buffer_T start = buffer_new(); + buffer_T start; + buffer_init(&start, 128); json_start_object(&json, "start"); json_add_size_t(&start, "line", token->location.start.line); json_add_size_t(&start, "column", token->location.start.column); buffer_concat(&json, &start); - buffer_free(&start); + free(start.value); json_end_object(&json); - buffer_T end = buffer_new(); + buffer_T end; + buffer_init(&end, 128); json_start_object(&json, "end"); json_add_size_t(&end, "line", token->location.end.line); json_add_size_t(&end, "column", token->location.end.column); buffer_concat(&json, &end); - buffer_free(&end); + free(end.value); json_end_object(&json); json_end_object(&json); diff --git a/test/c/test_buffer.c b/test/c/test_buffer.c index 054b2d698..58bce8358 100644 --- a/test/c/test_buffer.c +++ b/test/c/test_buffer.c @@ -5,18 +5,19 @@ TEST(test_buffer_init) buffer_T buffer; - ck_assert(buffer_init(&buffer)); + ck_assert(buffer_init(&buffer, 1024)); ck_assert_int_eq(buffer.capacity, 1024); ck_assert_int_eq(buffer.length, 0); ck_assert_ptr_nonnull(buffer.value); ck_assert_str_eq(buffer.value, ""); - buffer_free(&buffer); + free(buffer.value); END // Test appending text to buffer TEST(test_buffer_append) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_str_eq(buffer.value, ""); @@ -28,25 +29,28 @@ TEST(test_buffer_append) ck_assert_str_eq(buffer.value, "Hello World"); ck_assert_int_eq(buffer.length, 11); - buffer_free(&buffer); + free(buffer.value); END // Test prepending text to buffer TEST(test_buffer_prepend) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); buffer_append(&buffer, "World"); buffer_prepend(&buffer, "Hello "); ck_assert_str_eq(buffer.value, "Hello World"); ck_assert_int_eq(buffer.length, 11); - buffer_free(&buffer); + free(buffer.value); END // Test concatenating two buffers TEST(test_buffer_concat) - buffer_T buffer1 = buffer_new(); - buffer_T buffer2 = buffer_new(); + buffer_T buffer1; + buffer_init(&buffer1, 1024); + buffer_T buffer2; + buffer_init(&buffer2, 1024); buffer_append(&buffer1, "Hello"); buffer_append(&buffer2, " World"); @@ -55,13 +59,14 @@ TEST(test_buffer_concat) ck_assert_str_eq(buffer1.value, "Hello World"); ck_assert_int_eq(buffer1.length, 11); - buffer_free(&buffer1); - buffer_free(&buffer2); + free(buffer1.value); + free(buffer2.value); END // Test increating TEST(test_buffer_increase_capacity) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -71,12 +76,13 @@ TEST(test_buffer_increase_capacity) ck_assert(buffer_increase_capacity(&buffer, 1024 + 1)); ck_assert_int_eq(buffer.capacity, 2050); - buffer_free(&buffer); + free(buffer.value); END // Test expanding capacity TEST(test_buffer_expand_capacity) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -89,12 +95,13 @@ TEST(test_buffer_expand_capacity) ck_assert(buffer_expand_capacity(&buffer)); ck_assert_int_eq(buffer.capacity, 8192); - buffer_free(&buffer); + free(buffer.value); END // Test expanding if needed TEST(test_buffer_expand_if_needed) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -110,11 +117,12 @@ TEST(test_buffer_expand_if_needed) ck_assert(buffer_expand_if_needed(&buffer, 1025)); ck_assert_int_eq(buffer.capacity, 3074); // initial capacity (1024) + (required (1025) * 2) = 3074 - buffer_free(&buffer); + free(buffer.value); END TEST(test_buffer_expand_if_needed_with_nearly_full_buffer) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -124,12 +132,13 @@ TEST(test_buffer_expand_if_needed_with_nearly_full_buffer) ck_assert(buffer_expand_if_needed(&buffer, 2)); ck_assert_int_eq(buffer.capacity, 2048); - buffer_free(&buffer); + free(buffer.value); END // Test resizing buffer TEST(test_buffer_resize) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -142,12 +151,13 @@ TEST(test_buffer_resize) ck_assert(buffer_resize(&buffer, 8192)); ck_assert_int_eq(buffer.capacity, 8192); - buffer_free(&buffer); + free(buffer.value); END // Test clearing buffer without freeing memory TEST(test_buffer_clear) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); ck_assert_int_eq(buffer.capacity, 1024); @@ -162,17 +172,21 @@ TEST(test_buffer_clear) ck_assert_int_eq(buffer.length, 0); ck_assert_int_eq(buffer.capacity, 1024); // Capacity should remain unchanged - buffer_free(&buffer); + free(buffer.value); END // Test freeing buffer TEST(test_buffer_free) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); buffer_append(&buffer, "Test"); ck_assert_int_eq(buffer.length, 4); ck_assert_int_eq(buffer.capacity, 1024); - buffer_free(&buffer); + free(buffer.value); + buffer.value = NULL; + buffer.length = 0; + buffer.capacity = 0; ck_assert_ptr_null(buffer.value); ck_assert_int_eq(buffer.length, 0); @@ -181,7 +195,8 @@ END // Test buffer UTF-8 integrity TEST(test_buffer_utf8_integrity) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); // UTF-8 String const char *utf8_text = "こんにちは"; @@ -192,12 +207,13 @@ TEST(test_buffer_utf8_integrity) ck_assert_int_eq(buffer.length, 15); ck_assert_str_eq(buffer.value, utf8_text); - buffer_free(&buffer); + free(buffer.value); END // Test: Buffer Appending UTF-8 Characters TEST(test_buffer_append_utf8) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); // Append UTF-8 string buffer_append(&buffer, "こんにちは"); // "Hello" in Japanese @@ -205,12 +221,13 @@ TEST(test_buffer_append_utf8) ck_assert_int_eq(buffer_length(&buffer), 15); ck_assert_str_eq(buffer_value(&buffer), "こんにちは"); - buffer_free(&buffer); + free(buffer.value); END // Test buffer length correctness TEST(test_buffer_length_correctness) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); buffer_append(&buffer, "Short"); size_t length = buffer_length(&buffer); @@ -220,17 +237,18 @@ TEST(test_buffer_length_correctness) length = buffer_length(&buffer); ck_assert_int_eq(length, 12); - buffer_free(&buffer); + free(buffer.value); END // Test: Buffer Null-Termination TEST(test_buffer_null_termination) - buffer_T buffer = buffer_new(); + buffer_T buffer; + buffer_init(&buffer, 1024); buffer_append(&buffer, "Test"); ck_assert(buffer_value(&buffer)[buffer_length(&buffer)] == '\0'); // Ensure null termination - buffer_free(&buffer); + free(buffer.value); END TCase *buffer_tests(void) { diff --git a/test/c/test_json.c b/test/c/test_json.c index 8dec4b990..d734b84f8 100644 --- a/test/c/test_json.c +++ b/test/c/test_json.c @@ -5,7 +5,8 @@ #include "../../src/include/json.h" TEST(test_json_escape_basic) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "key", "value"); @@ -13,11 +14,12 @@ TEST(test_json_escape_basic) ck_assert_str_eq(buffer_value(&json), "{\"key\": \"value\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_escape_quotes) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "quote", "This is a \"quoted\" string"); @@ -25,11 +27,12 @@ TEST(test_json_escape_quotes) ck_assert_str_eq(buffer_value(&json), "{\"quote\": \"This is a \\\"quoted\\\" string\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_escape_backslash) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "path", "C:\\Users\\Test"); @@ -37,11 +40,12 @@ TEST(test_json_escape_backslash) ck_assert_str_eq(buffer_value(&json), "{\"path\": \"C:\\\\Users\\\\Test\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_escape_newline) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "text", "Line1\nLine2"); @@ -49,11 +53,12 @@ TEST(test_json_escape_newline) ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Line1\\nLine2\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_escape_tab) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "text", "Column1\tColumn2"); @@ -61,11 +66,12 @@ TEST(test_json_escape_tab) ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Column1\\tColumn2\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_escape_mixed) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); json_add_string(&json, "complex", "A \"quoted\" \\ path\nwith\ttabs."); @@ -73,11 +79,12 @@ TEST(test_json_escape_mixed) ck_assert_str_eq(buffer_value(&json), "{\"complex\": \"A \\\"quoted\\\" \\\\ path\\nwith\\ttabs.\"}"); - buffer_free(&json); + free(json.value); END TEST(test_json_root_object) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); @@ -86,14 +93,16 @@ TEST(test_json_root_object) json_add_double(&json, "score", 99.5); json_add_bool(&json, "active", 1); - buffer_T address = buffer_new(); + buffer_T address; + buffer_init(&address, 1024); json_start_object(&json, "address"); json_add_string(&address, "city", "Basel"); json_add_string(&address, "country", "Switzerland"); buffer_concat(&json, &address); json_end_object(&json); - buffer_T languages = buffer_new(); + buffer_T languages; + buffer_init(&languages, 1024); json_start_array(&json, "languages"); json_add_string(&languages, NULL, "Ruby"); json_add_string(&languages, NULL, "C"); @@ -101,7 +110,8 @@ TEST(test_json_root_object) buffer_concat(&json, &languages); json_end_array(&json); - buffer_T ratings = buffer_new(); + buffer_T ratings; + buffer_init(&ratings, 1024); json_start_array(&json, "ratings"); json_add_double(&ratings, NULL, 4.5); json_add_int(&ratings, NULL, 3); @@ -115,11 +125,15 @@ TEST(test_json_root_object) ck_assert_str_eq(buffer_value(&json), "{\"name\": \"John\", \"age\": 20, \"score\": 99.50, \"active\": true, \"address\": {\"city\": \"Basel\", \"country\": \"Switzerland\"}, \"languages\": [\"Ruby\", \"C\", \"JavaScript\"], \"ratings\": [4.50, 3, 5.0, 3.79, 5]}"); - buffer_free(&json); + free(address.value); + free(languages.value); + free(ratings.value); + free(json.value); END TEST(test_json_root_array) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_array(&json); @@ -135,19 +149,22 @@ TEST(test_json_root_array) ck_assert_str_eq(buffer_value(&json), "[\"Ruby\", \"C\", \"JavaScript\", 42, 3.14, true, false]"); - buffer_free(&json); + free(json.value); END TEST(test_json_append_array_to_object) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); - buffer_T object = buffer_new(); + buffer_T object; + buffer_init(&object, 1024); json_start_object(&json, "object"); json_add_string(&object, "key", "value"); - buffer_T array = buffer_new(); + buffer_T array; + buffer_init(&array, 1024); json_start_array(&object, "array"); json_add_string(&array, NULL, "One"); json_add_string(&array, NULL, "Two"); @@ -162,20 +179,25 @@ TEST(test_json_append_array_to_object) ck_assert_str_eq(buffer_value(&json), "{\"object\": {\"key\": \"value\", \"array\": [\"One\", \"Two\"]}}"); - buffer_free(&json); + free(array.value); + free(object.value); + free(json.value); END TEST(test_json_append_object_array) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_start_root_object(&json); - buffer_T array = buffer_new(); + buffer_T array; + buffer_init(&array, 1024); json_start_array(&json, "array"); json_add_string(&array, NULL, "One"); json_add_string(&array, NULL, "Two"); - buffer_T object = buffer_new(); + buffer_T object; + buffer_init(&object, 1024); json_start_object(&array, NULL); json_add_string(&object, "key", "value"); @@ -189,7 +211,9 @@ TEST(test_json_append_object_array) ck_assert_str_eq(buffer_value(&json), "{\"array\": [\"One\", \"Two\", {\"key\": \"value\"}]}"); - buffer_free(&json); + free(object.value); + free(array.value); + free(json.value); END TEST(test_json_double_to_string_precision) @@ -248,16 +272,18 @@ TEST(test_json_int_to_string_min_max) END TEST(test_json_add_size_t_basic) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_add_size_t(&json, "size", 42); ck_assert_str_eq(buffer_value(&json), "\"size\": 42"); - buffer_free(&json); + free(json.value); END TEST(test_json_add_size_t_large_number) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_add_size_t(&json, "size", 9876543210UL); ck_assert_str_eq(buffer_value(&json), "\"size\": 9876543210"); @@ -266,11 +292,12 @@ TEST(test_json_add_size_t_large_number) json_add_size_t(&json, "size", SIZE_MAX); ck_assert_str_eq(buffer_value(&json), "\"size\": 18446744073709551615"); - buffer_free(&json); + free(json.value); END TEST(test_json_add_size_t_in_array) - buffer_T json = buffer_new(); + buffer_T json; + buffer_init(&json, 1024); json_add_size_t(&json, NULL, 1024); json_add_size_t(&json, NULL, 2048); @@ -278,7 +305,7 @@ TEST(test_json_add_size_t_in_array) ck_assert_str_eq(buffer_value(&json), "1024, 2048, 4096"); - buffer_free(&json); + free(json.value); END diff --git a/test/c/test_lex.c b/test/c/test_lex.c index d0ed240a0..ee2418c9f 100644 --- a/test/c/test_lex.c +++ b/test/c/test_lex.c @@ -3,18 +3,20 @@ TEST(herb_lex_to_buffer_empty_file) char* html = ""; - buffer_T output = buffer_new(); + buffer_T output; + buffer_init(&output, 1024); herb_lex_to_buffer(html, &output); ck_assert_str_eq(output.value, "#\" range=[0, 0] start=(1:0) end=(1:0)>\n"); - buffer_free(&output); + free(output.value); END TEST(herb_lex_to_buffer_basic_tag) char* html = ""; - buffer_T output = buffer_new(); + buffer_T output; + buffer_init(&output, 1024); herb_lex_to_buffer(html, &output); @@ -29,7 +31,7 @@ TEST(herb_lex_to_buffer_basic_tag) "#\" range=[13, 13] start=(1:13) end=(1:13)>\n" ); - buffer_free(&output); + free(output.value); END TCase *lex_tests(void) { diff --git a/test/c/test_token.c b/test/c/test_token.c index a0d06daa2..d8ea10fd5 100644 --- a/test/c/test_token.c +++ b/test/c/test_token.c @@ -8,7 +8,8 @@ TEST(test_token) END TEST(test_token_to_string) - buffer_T output = buffer_new(); + buffer_T output; + buffer_init(&output, 1024); herb_lex_to_buffer("hello", &output); ck_assert_str_eq( @@ -17,11 +18,12 @@ TEST(test_token_to_string) "#\" range=[5, 5] start=(1:5) end=(1:5)>\n" ); - buffer_free(&output); + free(output.value); END TEST(test_token_to_json) - buffer_T output = buffer_new(); + buffer_T output; + buffer_init(&output, 1024); herb_lex_json_to_buffer("hello", &output); const char* expected = "[" @@ -31,7 +33,7 @@ TEST(test_token_to_json) ck_assert_str_eq(output.value, expected); - buffer_free(&output); + free(output.value); END TCase *token_tests(void) { diff --git a/wasm/herb-wasm.cpp b/wasm/herb-wasm.cpp index 91d270fdb..9d8624468 100644 --- a/wasm/herb-wasm.cpp +++ b/wasm/herb-wasm.cpp @@ -61,19 +61,21 @@ val Herb_parse(const std::string& source, val options) { std::string Herb_extract_ruby(const std::string& source) { buffer_T output; - buffer_init(&output); + buffer_init(&output, source.length()); + herb_extract_ruby_to_buffer(source.c_str(), &output); std::string result(buffer_value(&output)); - buffer_free(&output); + free(output.value); return result; } std::string Herb_extract_html(const std::string& source) { buffer_T output; - buffer_init(&output); + buffer_init(&output, source.length()); + herb_extract_html_to_buffer(source.c_str(), &output); std::string result(buffer_value(&output)); - buffer_free(&output); + free(output.value); return result; } From b26978aedc928cda900e7d44dbf261e88e414c02 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 8 Oct 2025 12:36:45 +0200 Subject: [PATCH 23/97] C: Remove JSON Serialize Implementation (#586) This pull request removes the JSON Serialize Implementation that we haven't really made use of, so we are going to remove it for now. If we end up needing it again, we can reference back to this pull request and add the implementation back as it should be somewhat straightforward to bring back. --- CONTRIBUTING.md | 1 - ext/herb/extension.c | 16 - javascript/packages/node/binding.gyp | 1 - javascript/packages/node/extension/herb.cpp | 32 -- lib/herb/libherb.rb | 1 - lib/herb/libherb/libherb.rb | 8 - src/herb.c | 22 -- src/include/herb.h | 1 - src/include/json.h | 28 -- src/include/token.h | 1 - src/json.c | 205 ------------ src/main.c | 12 - src/token.c | 41 --- test/c/main.c | 2 - test/c/test_json.c | 333 -------------------- test/c/test_token.c | 16 - 16 files changed, 720 deletions(-) delete mode 100644 src/include/json.h delete mode 100644 src/json.c delete mode 100644 test/c/test_json.c diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0bdc6276..17cecb7a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,6 @@ The `herb` executable exposes a few commands for interacting with `.html.erb` fi Herb 🌿 Powerful and seamless HTML-aware ERB parsing and tooling. ./herb lex [file] - Lex a file -./herb lex_json [file] - Lex a file and return the result as json. ./herb parse [file] - Parse a file ./herb ruby [file] - Extract Ruby from a file ./herb html [file] - Extract HTML from a file diff --git a/ext/herb/extension.c b/ext/herb/extension.c index 9b2884aee..d50931f9e 100644 --- a/ext/herb/extension.c +++ b/ext/herb/extension.c @@ -85,21 +85,6 @@ static VALUE Herb_parse_file(VALUE self, VALUE path) { return result; } -static VALUE Herb_lex_to_json(VALUE self, VALUE source) { - char* string = (char*) check_string(source); - buffer_T output; - - if (!buffer_init(&output, 4096)) { return Qnil; } - - herb_lex_json_to_buffer(string, &output); - - VALUE result = rb_str_new(output.value, output.length); - - free(output.value); - - return result; -} - static VALUE Herb_extract_ruby(VALUE self, VALUE source) { char* string = (char*) check_string(source); buffer_T output; @@ -151,7 +136,6 @@ void Init_herb(void) { rb_define_singleton_method(mHerb, "lex", Herb_lex, 1); rb_define_singleton_method(mHerb, "parse_file", Herb_parse_file, 1); rb_define_singleton_method(mHerb, "lex_file", Herb_lex_file, 1); - rb_define_singleton_method(mHerb, "lex_to_json", Herb_lex_to_json, 1); rb_define_singleton_method(mHerb, "extract_ruby", Herb_extract_ruby, 1); rb_define_singleton_method(mHerb, "extract_html", Herb_extract_html, 1); rb_define_singleton_method(mHerb, "version", Herb_version, 0); diff --git a/javascript/packages/node/binding.gyp b/javascript/packages/node/binding.gyp index 8afbd6559..9bba520c9 100644 --- a/javascript/packages/node/binding.gyp +++ b/javascript/packages/node/binding.gyp @@ -24,7 +24,6 @@ "./extension/libherb/herb.c", "./extension/libherb/html_util.c", "./extension/libherb/io.c", - "./extension/libherb/json.c", "./extension/libherb/lexer_peek_helpers.c", "./extension/libherb/lexer.c", "./extension/libherb/location.c", diff --git a/javascript/packages/node/extension/herb.cpp b/javascript/packages/node/extension/herb.cpp index a3e080902..e470ab027 100644 --- a/javascript/packages/node/extension/herb.cpp +++ b/javascript/packages/node/extension/herb.cpp @@ -142,37 +142,6 @@ napi_value Herb_parse_file(napi_env env, napi_callback_info info) { return result; } -napi_value Herb_lex_to_json(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); - - if (argc < 1) { - napi_throw_error(env, nullptr, "Wrong number of arguments"); - return nullptr; - } - - char* string = CheckString(env, args[0]); - if (!string) { return nullptr; } - - buffer_T output; - if (!buffer_init(&output, 4096)) { - free(string); - napi_throw_error(env, nullptr, "Failed to initialize buffer"); - return nullptr; - } - - herb_lex_json_to_buffer(string, &output); - - napi_value result; - napi_create_string_utf8(env, output.value, output.length, &result); - - free(output.value); - free(string); - - return result; -} - napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value args[1]; @@ -252,7 +221,6 @@ napi_value Init(napi_env env, napi_value exports) { { "lex", nullptr, Herb_lex, nullptr, nullptr, nullptr, napi_default, nullptr }, { "parseFile", nullptr, Herb_parse_file, nullptr, nullptr, nullptr, napi_default, nullptr }, { "lexFile", nullptr, Herb_lex_file, nullptr, nullptr, nullptr, napi_default, nullptr }, - { "lexToJson", nullptr, Herb_lex_to_json, nullptr, nullptr, nullptr, napi_default, nullptr }, { "extractRuby", nullptr, Herb_extract_ruby, nullptr, nullptr, nullptr, napi_default, nullptr }, { "extractHTML", nullptr, Herb_extract_html, nullptr, nullptr, nullptr, napi_default, nullptr }, { "version", nullptr, Herb_version, nullptr, nullptr, nullptr, napi_default, nullptr }, diff --git a/lib/herb/libherb.rb b/lib/herb/libherb.rb index 401004303..ea670bc00 100644 --- a/lib/herb/libherb.rb +++ b/lib/herb/libherb.rb @@ -25,7 +25,6 @@ def self.library_path ffi_lib(library_path) attach_function :herb_lex_to_buffer, [:pointer, :pointer], :void - attach_function :herb_lex_json_to_buffer, [:pointer, :pointer], :void attach_function :herb_lex, [:pointer], :pointer attach_function :herb_parse, [:pointer], :pointer attach_function :herb_extract_ruby_to_buffer, [:pointer, :pointer], :void diff --git a/lib/herb/libherb/libherb.rb b/lib/herb/libherb/libherb.rb index 38354e405..03c3b77d7 100644 --- a/lib/herb/libherb/libherb.rb +++ b/lib/herb/libherb/libherb.rb @@ -26,14 +26,6 @@ def self.lex(source) ) end - def self.lex_to_json(source) - LibHerb::Buffer.with do |output| - LibHerb.herb_lex_json_to_buffer(source, output.pointer) - - JSON.parse(output.read.force_encoding("utf-8")) - end - end - def self.extract_ruby(source) LibHerb::Buffer.with do |output| LibHerb.herb_extract_ruby_to_buffer(source, output.pointer) diff --git a/src/herb.c b/src/herb.c index 1348c9495..dab7971be 100644 --- a/src/herb.c +++ b/src/herb.c @@ -2,7 +2,6 @@ #include "include/array.h" #include "include/buffer.h" #include "include/io.h" -#include "include/json.h" #include "include/lexer.h" #include "include/parser.h" #include "include/token.h" @@ -65,27 +64,6 @@ void herb_lex_to_buffer(const char* source, buffer_T* output) { herb_free_tokens(&tokens); } -void herb_lex_json_to_buffer(const char* source, buffer_T* output) { - array_T* tokens = herb_lex(source); - - buffer_T json; - buffer_init(&json, 4096); - json_start_root_array(&json); - - for (size_t i = 0; i < array_size(tokens); i++) { - token_T* token = array_get(tokens, i); - char* token_json = token_to_json(token); - json_add_raw_string(&json, token_json); - free(token_json); - } - - json_end_array(&json); - buffer_concat(output, &json); - - free(json.value); - herb_free_tokens(&tokens); -} - void herb_free_tokens(array_T** tokens) { if (!tokens || !*tokens) { return; } diff --git a/src/include/herb.h b/src/include/herb.h index d4ec2c7c6..ef886a087 100644 --- a/src/include/herb.h +++ b/src/include/herb.h @@ -14,7 +14,6 @@ extern "C" { #endif void herb_lex_to_buffer(const char* source, buffer_T* output); -void herb_lex_json_to_buffer(const char* source, buffer_T* output); array_T* herb_lex(const char* source); array_T* herb_lex_file(const char* path); diff --git a/src/include/json.h b/src/include/json.h deleted file mode 100644 index b48428c3a..000000000 --- a/src/include/json.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef HERB_JSON_H -#define HERB_JSON_H - -#include "buffer.h" - -void json_start_root_object(buffer_T* json); -void json_start_root_array(buffer_T* json); - -void json_escape_string(buffer_T* json, const char* string); - -void json_add_string(buffer_T* json, const char* key, const char* value); -void json_add_int(buffer_T* json, const char* key, int value); -void json_add_size_t(buffer_T* json, const char* key, size_t value); -void json_add_double(buffer_T* json, const char* key, double value); -void json_add_bool(buffer_T* json, const char* key, int value); - -void json_add_raw_string(buffer_T* json, const char* string); - -void json_start_object(buffer_T* json, const char* key); -void json_end_object(buffer_T* json); - -void json_start_array(buffer_T* json, const char* key); -void json_end_array(buffer_T* json); - -void json_double_to_string(double value, char* buffer); -void json_int_to_string(int value, char* buffer); - -#endif diff --git a/src/include/token.h b/src/include/token.h index 523dd1f04..5628e2f0d 100644 --- a/src/include/token.h +++ b/src/include/token.h @@ -7,7 +7,6 @@ token_T* token_init(const char* value, token_type_T type, lexer_T* lexer); char* token_to_string(const token_T* token); -char* token_to_json(const token_T* token); const char* token_type_to_string(token_type_T type); char* token_value(const token_T* token); diff --git a/src/json.c b/src/json.c deleted file mode 100644 index a1bc80662..000000000 --- a/src/json.c +++ /dev/null @@ -1,205 +0,0 @@ -#include "include/json.h" -#include "include/buffer.h" - -void json_escape_string(buffer_T* json, const char* string) { - if (!string) { - buffer_append(json, "null"); - return; - } - - buffer_append(json, "\""); - - while (*string) { - switch (*string) { - case '\"': buffer_append(json, "\\\""); break; - case '\\': buffer_append(json, "\\\\"); break; - case '\n': buffer_append(json, "\\n"); break; - case '\t': buffer_append(json, "\\t"); break; - default: buffer_append_char(json, *string); break; - } - string++; - } - - buffer_append(json, "\""); -} - -void json_int_to_string(const int value, char* buffer) { - char string[20]; // Enough to hold all possible int values - int i = 0; - - // Handle negative numbers - unsigned int abs_value = (unsigned int) abs(value); - - do { - string[i++] = (char) ((abs_value % 10) + '0'); - abs_value /= 10; - } while (abs_value > 0); - - if (value < 0) { string[i++] = '-'; } - - int j = 0; - - while (i > 0) { - buffer[j++] = string[--i]; - } - - buffer[j] = '\0'; -} - -void json_double_to_string(const double value, char* buffer) { - const int int_part = (int) value; - const double frac_part = value - (double) int_part; - const int frac_as_int = (int) (frac_part * 100); // Keep 2 decimal places - - char int_buffer[20]; - char frac_buffer[5]; - - json_int_to_string(int_part, int_buffer); - json_int_to_string(frac_as_int < 0 ? -frac_as_int : frac_as_int, frac_buffer); - - char* pointer = buffer; - for (const char* source = int_buffer; *source != '\0'; ++source) { - *pointer++ = *source; - } - - *pointer++ = '.'; - - for (const char* source = frac_buffer; *source != '\0'; ++source) { - *pointer++ = *source; - } - - *pointer = '\0'; -} - -void json_add_string(buffer_T* json, const char* key, const char* value) { - if (!json) { return; } - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - json_escape_string(json, value); -} - -void json_add_double(buffer_T* json, const char* key, const double value) { - if (!json) { return; } - - char number[32]; - json_double_to_string(value, number); - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - buffer_append(json, number); -} - -void json_add_int(buffer_T* json, const char* key, const int value) { - if (!json) { return; } - - char number[20]; - json_int_to_string(value, number); - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - buffer_append(json, number); - if (json->length == 1) { buffer_append(json, " "); } -} - -void json_add_size_t(buffer_T* json, const char* key, size_t value) { - if (!json) { return; } - - char number[32]; - char temp[32]; - int i = 0; - - do { - temp[i++] = (char) ((value % 10) + '0'); - value /= 10; - } while (value > 0); - - int j = 0; - while (i > 0) { - number[j++] = temp[--i]; - } - number[j] = '\0'; - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - buffer_append(json, number); - if (json->length == 1) { buffer_append(json, " "); } -} - -void json_add_bool(buffer_T* json, const char* key, const int value) { - if (!json) { return; } - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - buffer_append(json, value ? "true" : "false"); -} - -void json_add_raw_string(buffer_T* json, const char* string) { - if (!json) { return; } - - if (json->length > 1) { buffer_append(json, ", "); } - - buffer_append(json, string); -} - -void json_start_root_object(buffer_T* json) { - if (json) { buffer_append(json, "{"); } -} - -void json_start_object(buffer_T* json, const char* key) { - if (!json) { return; } - - if (json->length > 1) { buffer_append(json, ", "); } - - if (key) { - json_escape_string(json, key); - buffer_append(json, ": "); - } - - buffer_append(json, "{"); -} - -void json_end_object(buffer_T* json) { - if (json) { buffer_append(json, "}"); } -} - -void json_start_root_array(buffer_T* json) { - if (json) { buffer_append(json, "["); } -} - -void json_start_array(buffer_T* json, const char* key) { - if (!json) { return; } - - if (json->length > 1) { buffer_append(json, ", "); } - json_escape_string(json, key); - buffer_append(json, ": ["); -} - -void json_end_array(buffer_T* json) { - if (json) { buffer_append(json, "]"); } -} diff --git a/src/main.c b/src/main.c index 2e1efbfa2..e30b9e5f6 100644 --- a/src/main.c +++ b/src/main.c @@ -39,7 +39,6 @@ int main(const int argc, char* argv[]) { printf("Herb 🌿 Powerful and seamless HTML-aware ERB parsing and tooling.\n\n"); printf("./herb lex [file] - Lex a file\n"); - printf("./herb lex_json [file] - Lex a file and return the result as json.\n"); printf("./herb parse [file] - Parse a file\n"); printf("./herb ruby [file] - Extract Ruby from a file\n"); printf("./herb html [file] - Extract HTML from a file\n"); @@ -93,17 +92,6 @@ int main(const int argc, char* argv[]) { return 0; } - if (strcmp(argv[1], "lex_json") == 0) { - herb_lex_json_to_buffer(source, &output); - - printf("%s\n", output.value); - - free(output.value); - free(source); - - return 0; - } - if (strcmp(argv[1], "parse") == 0) { AST_DOCUMENT_NODE_T* root = herb_parse(source, NULL); diff --git a/src/token.c b/src/token.c index 60a5010ac..b84362754 100644 --- a/src/token.c +++ b/src/token.c @@ -1,5 +1,4 @@ #include "include/token.h" -#include "include/json.h" #include "include/lexer.h" #include "include/position.h" #include "include/range.h" @@ -119,46 +118,6 @@ char* token_to_string(const token_T* token) { return string; } -char* token_to_json(const token_T* token) { - buffer_T json; - buffer_init(&json, 512); - - json_start_root_object(&json); - json_add_string(&json, "type", token_type_to_string(token->type)); - json_add_string(&json, "value", token->value); - - buffer_T range; - buffer_init(&range, 128); - json_start_array(&json, "range"); - json_add_size_t(&range, NULL, token->range.from); - json_add_size_t(&range, NULL, token->range.to); - buffer_concat(&json, &range); - free(range.value); - json_end_array(&json); - - buffer_T start; - buffer_init(&start, 128); - json_start_object(&json, "start"); - json_add_size_t(&start, "line", token->location.start.line); - json_add_size_t(&start, "column", token->location.start.column); - buffer_concat(&json, &start); - free(start.value); - json_end_object(&json); - - buffer_T end; - buffer_init(&end, 128); - json_start_object(&json, "end"); - json_add_size_t(&end, "line", token->location.end.line); - json_add_size_t(&end, "column", token->location.end.column); - buffer_concat(&json, &end); - free(end.value); - json_end_object(&json); - - json_end_object(&json); - - return buffer_value(&json); -} - char* token_value(const token_T* token) { return token->value; } diff --git a/test/c/main.c b/test/c/main.c index c61106da6..200d92a6b 100644 --- a/test/c/main.c +++ b/test/c/main.c @@ -6,7 +6,6 @@ TCase *buffer_tests(void); TCase *herb_tests(void); TCase *html_util_tests(void); TCase *io_tests(void); -TCase *json_tests(void); TCase *lex_tests(void); TCase *token_tests(void); TCase *util_tests(void); @@ -19,7 +18,6 @@ Suite *herb_suite(void) { suite_add_tcase(suite, herb_tests()); suite_add_tcase(suite, html_util_tests()); suite_add_tcase(suite, io_tests()); - suite_add_tcase(suite, json_tests()); suite_add_tcase(suite, lex_tests()); suite_add_tcase(suite, token_tests()); suite_add_tcase(suite, util_tests()); diff --git a/test/c/test_json.c b/test/c/test_json.c deleted file mode 100644 index d734b84f8..000000000 --- a/test/c/test_json.c +++ /dev/null @@ -1,333 +0,0 @@ -#include - -#include "include/test.h" -#include "../../src/include/buffer.h" -#include "../../src/include/json.h" - -TEST(test_json_escape_basic) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "key", "value"); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"key\": \"value\"}"); - - free(json.value); -END - -TEST(test_json_escape_quotes) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "quote", "This is a \"quoted\" string"); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"quote\": \"This is a \\\"quoted\\\" string\"}"); - - free(json.value); -END - -TEST(test_json_escape_backslash) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "path", "C:\\Users\\Test"); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"path\": \"C:\\\\Users\\\\Test\"}"); - - free(json.value); -END - -TEST(test_json_escape_newline) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "text", "Line1\nLine2"); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Line1\\nLine2\"}"); - - free(json.value); -END - -TEST(test_json_escape_tab) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "text", "Column1\tColumn2"); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Column1\\tColumn2\"}"); - - free(json.value); -END - -TEST(test_json_escape_mixed) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - json_add_string(&json, "complex", "A \"quoted\" \\ path\nwith\ttabs."); - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"complex\": \"A \\\"quoted\\\" \\\\ path\\nwith\\ttabs.\"}"); - - free(json.value); -END - -TEST(test_json_root_object) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - - json_add_string(&json, "name", "John"); - json_add_int(&json, "age", 20); - json_add_double(&json, "score", 99.5); - json_add_bool(&json, "active", 1); - - buffer_T address; - buffer_init(&address, 1024); - json_start_object(&json, "address"); - json_add_string(&address, "city", "Basel"); - json_add_string(&address, "country", "Switzerland"); - buffer_concat(&json, &address); - json_end_object(&json); - - buffer_T languages; - buffer_init(&languages, 1024); - json_start_array(&json, "languages"); - json_add_string(&languages, NULL, "Ruby"); - json_add_string(&languages, NULL, "C"); - json_add_string(&languages, NULL, "JavaScript"); - buffer_concat(&json, &languages); - json_end_array(&json); - - buffer_T ratings; - buffer_init(&ratings, 1024); - json_start_array(&json, "ratings"); - json_add_double(&ratings, NULL, 4.5); - json_add_int(&ratings, NULL, 3); - json_add_double(&ratings, NULL, 5.0); - json_add_double(&ratings, NULL, 3.8); - json_add_int(&ratings, NULL, 5); - buffer_concat(&json, &ratings); - json_end_array(&json); - - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"name\": \"John\", \"age\": 20, \"score\": 99.50, \"active\": true, \"address\": {\"city\": \"Basel\", \"country\": \"Switzerland\"}, \"languages\": [\"Ruby\", \"C\", \"JavaScript\"], \"ratings\": [4.50, 3, 5.0, 3.79, 5]}"); - - free(address.value); - free(languages.value); - free(ratings.value); - free(json.value); -END - -TEST(test_json_root_array) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_array(&json); - - json_add_string(&json, NULL, "Ruby"); - json_add_string(&json, NULL, "C"); - json_add_string(&json, NULL, "JavaScript"); - json_add_int(&json, NULL, 42); - json_add_double(&json, NULL, 3.14159); - json_add_bool(&json, NULL, 1); - json_add_bool(&json, NULL, 0); - - json_end_array(&json); - - ck_assert_str_eq(buffer_value(&json), "[\"Ruby\", \"C\", \"JavaScript\", 42, 3.14, true, false]"); - - free(json.value); -END - -TEST(test_json_append_array_to_object) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - - buffer_T object; - buffer_init(&object, 1024); - json_start_object(&json, "object"); - json_add_string(&object, "key", "value"); - - buffer_T array; - buffer_init(&array, 1024); - json_start_array(&object, "array"); - json_add_string(&array, NULL, "One"); - json_add_string(&array, NULL, "Two"); - - buffer_concat(&object, &array); - json_end_array(&object); - - buffer_concat(&json, &object); - json_end_object(&json); - - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"object\": {\"key\": \"value\", \"array\": [\"One\", \"Two\"]}}"); - - free(array.value); - free(object.value); - free(json.value); -END - -TEST(test_json_append_object_array) - buffer_T json; - buffer_init(&json, 1024); - - json_start_root_object(&json); - - buffer_T array; - buffer_init(&array, 1024); - json_start_array(&json, "array"); - json_add_string(&array, NULL, "One"); - json_add_string(&array, NULL, "Two"); - - buffer_T object; - buffer_init(&object, 1024); - json_start_object(&array, NULL); - json_add_string(&object, "key", "value"); - - buffer_concat(&array, &object); - json_end_object(&array); - - buffer_concat(&json, &array); - json_end_array(&json); - - json_end_object(&json); - - ck_assert_str_eq(buffer_value(&json), "{\"array\": [\"One\", \"Two\", {\"key\": \"value\"}]}"); - - free(object.value); - free(array.value); - free(json.value); -END - -TEST(test_json_double_to_string_precision) - char buffer[64]; - - json_double_to_string(1.234567890123456, buffer); - ck_assert_str_eq(buffer, "1.23"); - - json_double_to_string(123456.7890123456789, buffer); - ck_assert_str_eq(buffer, "123456.78"); - - json_double_to_string(0.000000000000001, buffer); - ck_assert_str_eq(buffer, "0.0"); - - json_double_to_string(-42.987654321098765, buffer); - ck_assert_str_eq(buffer, "-42.98"); - - json_double_to_string(3.141592653589793, buffer); - ck_assert_str_eq(buffer, "3.14"); -END - -TEST(test_json_int_to_string_positive) - char buffer[20]; - - json_int_to_string(12345, buffer); - ck_assert_str_eq(buffer, "12345"); - - json_int_to_string(987654321, buffer); - ck_assert_str_eq(buffer, "987654321"); - - json_int_to_string(0, buffer); - ck_assert_str_eq(buffer, "0"); -END - -TEST(test_json_int_to_string_negative) - char buffer[20]; - - json_int_to_string(-1, buffer); - ck_assert_str_eq(buffer, "-1"); - - json_int_to_string(-42, buffer); - ck_assert_str_eq(buffer, "-42"); - - json_int_to_string(-987654321, buffer); - ck_assert_str_eq(buffer, "-987654321"); -END - -TEST(test_json_int_to_string_min_max) - char buffer[20]; - - json_int_to_string(2147483647, buffer); - ck_assert_str_eq(buffer, "2147483647"); - - json_int_to_string(-2147483648, buffer); - ck_assert_str_eq(buffer, "-2147483648"); -END - -TEST(test_json_add_size_t_basic) - buffer_T json; - buffer_init(&json, 1024); - - json_add_size_t(&json, "size", 42); - ck_assert_str_eq(buffer_value(&json), "\"size\": 42"); - - free(json.value); -END - -TEST(test_json_add_size_t_large_number) - buffer_T json; - buffer_init(&json, 1024); - - json_add_size_t(&json, "size", 9876543210UL); - ck_assert_str_eq(buffer_value(&json), "\"size\": 9876543210"); - - buffer_clear(&json); - json_add_size_t(&json, "size", SIZE_MAX); - ck_assert_str_eq(buffer_value(&json), "\"size\": 18446744073709551615"); - - free(json.value); -END - -TEST(test_json_add_size_t_in_array) - buffer_T json; - buffer_init(&json, 1024); - - json_add_size_t(&json, NULL, 1024); - json_add_size_t(&json, NULL, 2048); - json_add_size_t(&json, NULL, 4096); - - ck_assert_str_eq(buffer_value(&json), "1024, 2048, 4096"); - - free(json.value); -END - - -TCase* json_tests(void) { - TCase* json = tcase_create("JSON"); - - tcase_add_test(json, test_json_escape_basic); - tcase_add_test(json, test_json_escape_quotes); - tcase_add_test(json, test_json_escape_backslash); - tcase_add_test(json, test_json_escape_tab); - tcase_add_test(json, test_json_escape_mixed); - tcase_add_test(json, test_json_root_object); - tcase_add_test(json, test_json_root_array); - tcase_add_test(json, test_json_append_array_to_object); - tcase_add_test(json, test_json_append_object_array); - tcase_add_test(json, test_json_double_to_string_precision); - tcase_add_test(json, test_json_int_to_string_positive); - tcase_add_test(json, test_json_int_to_string_negative); - tcase_add_test(json, test_json_int_to_string_min_max); - tcase_add_test(json, test_json_add_size_t_basic); - tcase_add_test(json, test_json_add_size_t_large_number); - tcase_add_test(json, test_json_add_size_t_in_array); - - return json; -} diff --git a/test/c/test_token.c b/test/c/test_token.c index d8ea10fd5..946e0649f 100644 --- a/test/c/test_token.c +++ b/test/c/test_token.c @@ -21,27 +21,11 @@ TEST(test_token_to_string) free(output.value); END -TEST(test_token_to_json) - buffer_T output; - buffer_init(&output, 1024); - herb_lex_json_to_buffer("hello", &output); - - const char* expected = "[" - "{\"type\": \"TOKEN_IDENTIFIER\", \"value\": \"hello\", \"range\": [0 , 5], \"start\": {\"line\": 1, \"column\": 0}, \"end\": {\"line\": 1, \"column\": 5}}, " - "{\"type\": \"TOKEN_EOF\", \"value\": \"\", \"range\": [5 , 5], \"start\": {\"line\": 1, \"column\": 5}, \"end\": {\"line\": 1, \"column\": 5}}" - "]"; - - ck_assert_str_eq(output.value, expected); - - free(output.value); -END - TCase *token_tests(void) { TCase *token = tcase_create("Token"); tcase_add_test(token, test_token); tcase_add_test(token, test_token_to_string); - tcase_add_test(token, test_token_to_json); return token; } From 8d6adb1d25fc99634c01eb32c651cda685498cb4 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 8 Oct 2025 14:08:43 +0200 Subject: [PATCH 24/97] Linter: Add test helpers to simplify linter rule tests (#587) This pull request implements linter test helpers to reduce the verbosity in the linter rule tests. The new `createLinterTest()` helper provides a cleaner API with `expectError()`, `expectWarning()`, `expectNoOffenses()`, and `assertOffenses()` functions. Example: ```ts import { SomeRule } from "../../src/rules/some-rule.js" import { createLinterTest } from "../helpers/linter-test-helper.js" const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(SomeRule) describe("SomeRule", () => { test("no offenses", () => { expectNoOffenses(`
    `) }) test("with offenses", () => { expectError("Error message.") expectWarning("Warning message.") assertOffenses(`
    `) }) }) ``` Resolves #461 --- .../generators/app/templates/test.ts.ejs | 39 +- .../linter/test/helpers/linter-test-helper.ts | 222 ++++++++++ .../test/rules/erb-comment-syntax.test.ts | 64 +-- .../test/rules/erb-no-empty-tags.test.ts | 125 ++---- .../rules/erb-no-output-control-flow.test.ts | 90 +--- ...rb-no-silent-tag-in-attribute-name.test.ts | 97 ++--- .../rules/erb-prefer-image-tag-helper.test.ts | 276 ++---------- ...erb-require-whitespace-inside-tags.test.ts | 123 ++---- .../erb-requires-trailing-newline.test.ts | 132 ++---- .../linter/test/rules/erb-right-trim.test.ts | 190 ++++---- .../html-anchor-require-href-rule.test.ts | 71 +-- .../html-aria-attribute-must-be-valid.test.ts | 80 +--- .../html-aria-label-is-well-formatted.test.ts | 143 ++---- .../html-aria-level-must-be-valid.test.ts | 238 +++------- ...l-aria-role-heading-requires-level.test.ts | 33 +- .../html-aria-role-must-be-valid.test.ts | 48 +- .../html-attribute-double-quotes.test.ts | 141 ++---- .../html-attribute-equals-spacing.test.ts | 168 ++----- ...ml-attribute-values-require-quotes.test.ts | 106 +---- ...id-both-disabled-and-aria-disabled.test.ts | 147 ++----- .../html-boolean-attributes-no-value.test.ts | 126 ++---- .../test/rules/html-iframe-has-title.test.ts | 144 ++---- .../test/rules/html-img-require-alt.test.ts | 96 +--- .../rules/html-navigation-has-label.test.ts | 139 +----- .../html-no-aria-hidden-on-focusable.test.ts | 179 ++------ .../rules/html-no-block-inside-inline.test.ts | 240 +++------- .../html-no-duplicate-attributes.test.ts | 87 +--- .../test/rules/html-no-duplicate-ids.test.ts | 411 ++++-------------- .../rules/html-no-empty-attributes.test.ts | 334 +++----------- .../test/rules/html-no-empty-headings.test.ts | 268 ++---------- .../test/rules/html-no-nested-links.test.ts | 96 +--- .../rules/html-no-positive-tab-index.test.ts | 127 ++---- .../test/rules/html-no-self-closing.test.ts | 182 +++----- .../rules/html-no-title-attribute.test.ts | 152 ++----- ...-no-underscores-in-attribute-names.test.ts | 71 +-- .../rules/html-tag-name-lowercase.test.ts | 248 +++-------- .../test/rules/parser-no-errors.test.ts | 208 +++------ .../rules/svg-tag-name-capitalization.test.ts | 88 ++-- 38 files changed, 1477 insertions(+), 4252 deletions(-) create mode 100644 javascript/packages/linter/test/helpers/linter-test-helper.ts diff --git a/javascript/packages/linter/generators/linter-rule/generators/app/templates/test.ts.ejs b/javascript/packages/linter/generators/linter-rule/generators/app/templates/test.ts.ejs index 79bb036f0..ffe5f017f 100644 --- a/javascript/packages/linter/generators/linter-rule/generators/app/templates/test.ts.ejs +++ b/javascript/packages/linter/generators/linter-rule/generators/app/templates/test.ts.ejs @@ -1,44 +1,25 @@ import dedent from "dedent" - -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" - +import { describe, test } from "vitest" import { <%= ruleClassName %> } from "../../src/rules/<%= ruleName %>.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("<%= ruleClassName %>", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, expectWarning, assertOffenses } = createLinterTest(<%= ruleClassName %>) +describe("<%= ruleClassName %>", () => { test("valid case TODO", () => { - const html = dedent` + expectNoOffenses(dedent`

    <%%= title %>

    - ` - const linter = new Linter(Herb, [<%= ruleClassName %>]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("invalid case TODO", () => { - const html = dedent` + expectError("TODO: update rule message") + + assertOffenses(dedent`

    <%%= title %> - ` - const linter = new Linter(Herb, [<%= ruleClassName %>]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("<%= ruleName %>") - expect(lintResult.offenses[0].message).toBe("TODO: update rule message") - - // TODO: add assertions for invalid case + `) }) }) diff --git a/javascript/packages/linter/test/helpers/linter-test-helper.ts b/javascript/packages/linter/test/helpers/linter-test-helper.ts new file mode 100644 index 000000000..b4bf26ce9 --- /dev/null +++ b/javascript/packages/linter/test/helpers/linter-test-helper.ts @@ -0,0 +1,222 @@ +import { beforeAll, afterEach, expect } from "vitest" + +import { Herb } from "@herb-tools/node-wasm" +import { Linter } from "../../src/linter.js" + +import type { RuleClass } from "../../src/types.js" + +interface ExpectedLocation { + line?: number + column?: number +} + +type LocationInput = ExpectedLocation | [number, number] | [number] + +interface ExpectedOffense { + message: string + location?: ExpectedLocation +} + +interface LinterTestHelpers { + expectNoOffenses: (html: string, context?: any) => void + expectWarning: (message: string, location?: LocationInput) => void + expectError: (message: string, location?: LocationInput) => void + assertOffenses: (html: string, context?: any) => void +} + +/** + * Creates a test helper for linter rules that reduces boilerplate in tests. + * + * @param ruleClass - The rule class to test + * @returns Object with helper functions for testing + * + * @example + * ```ts + * const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(MyRule) + * + * test("valid case", () => { + * expectNoOffenses(`<%= title %>`) + * }) + * + * test("invalid case", () => { + * expectError("Error message", { line: 1, column: 1 }) + * // or use array syntax + * expectError("Error message", [1, 1]) + * + * assertOffenses(`<% %>`) + * }) + * ``` + */ +export function createLinterTest(ruleClass: RuleClass): LinterTestHelpers { + const expectedWarnings: ExpectedOffense[] = [] + const expectedErrors: ExpectedOffense[] = [] + let hasAsserted = false + + beforeAll(async () => { + await Herb.load() + }) + + afterEach(() => { + if (!hasAsserted && (expectedWarnings.length > 0 || expectedErrors.length > 0)) { + const pendingCount = expectedWarnings.length + expectedErrors.length + throw new Error( + `Test has ${pendingCount} pending expectation(s) that were never asserted. ` + + `Did you forget to call assertOffenses() or expectNoOffenses()?` + ) + } + + expectedWarnings.length = 0 + expectedErrors.length = 0 + hasAsserted = false + }) + + const expectNoOffenses = (html: string, context?: any) => { + if (expectedWarnings.length > 0 || expectedErrors.length > 0) { + throw new Error( + "Cannot call expectNoOffenses() after registering expectations with expectWarning() or expectError()" + ) + } + + hasAsserted = true + const linter = new Linter(Herb, [ruleClass]) + const lintResult = linter.lint(html, context) + + expect(lintResult.errors).toBe(0) + expect(lintResult.warnings).toBe(0) + expect(lintResult.offenses).toHaveLength(0) + } + + const normalizeLocation = (location?: LocationInput): ExpectedLocation | undefined => { + if (!location) return undefined + if (Array.isArray(location)) { + return location.length === 2 + ? { line: location[0], column: location[1] } + : { line: location[0] } + } + return location + } + + const expectWarning = (message: string, location?: LocationInput) => { + expectedWarnings.push({ message, location: normalizeLocation(location) }) + } + + const expectError = (message: string, location?: LocationInput) => { + expectedErrors.push({ message, location: normalizeLocation(location) }) + } + + const assertOffenses = (html: string, context?: any) => { + if (expectedWarnings.length === 0 && expectedErrors.length === 0) { + throw new Error( + "Cannot call assertOffenses() with no expectations. Use expectNoOffenses() instead." + ) + } + + hasAsserted = true + const linter = new Linter(Herb, [ruleClass]) + const lintResult = linter.lint(html, context) + + const ruleInstance = new ruleClass() + const ruleName = ruleInstance.name + + if (lintResult.errors !== expectedErrors.length) { + throw new Error( + `Expected ${expectedErrors.length} error(s) but found ${lintResult.errors}.\n` + + `Expected:\n${expectedErrors.map(e => ` - "${e.message}"`).join('\n')}\n` + + `Actual:\n${lintResult.offenses.filter(o => o.severity === 'error').map(o => ` - "${o.message}" at ${o.location.start.line}:${o.location.start.column}`).join('\n')}` + ) + } + + if (lintResult.warnings !== expectedWarnings.length) { + throw new Error( + `Expected ${expectedWarnings.length} warning(s) but found ${lintResult.warnings}.\n` + + `Expected:\n${expectedWarnings.map(w => ` - "${w.message}"`).join('\n')}\n` + + `Actual:\n${lintResult.offenses.filter(o => o.severity === 'warning').map(o => ` - "${o.message}" at ${o.location.start.line}:${o.location.start.column}`).join('\n')}` + ) + } + + lintResult.offenses.forEach(offense => { + expect(offense.rule).toBe(ruleName) + }) + + const actualErrors = lintResult.offenses.filter(o => o.severity === "error") + const actualWarnings = lintResult.offenses.filter(o => o.severity === "warning") + + matchOffenses(expectedErrors, actualErrors, "error") + matchOffenses(expectedWarnings, actualWarnings, "warning") + + expectedWarnings.length = 0 + expectedErrors.length = 0 + } + + return { + expectNoOffenses, + expectWarning, + expectError, + assertOffenses + } +} + +/** + * Matches expected offenses to actual offenses in an order-independent way + */ +function matchOffenses( + expected: ExpectedOffense[], + actual: any[], + severity: "error" | "warning" +) { + const unmatched = [...expected] + const unmatchedActual = [...actual] + + for (const actualOffense of actual) { + const matchIndex = unmatched.findIndex(exp => { + if (exp.message !== actualOffense.message) { + return false + } + + if (exp.location?.line !== undefined && exp.location.line !== actualOffense.location.start.line) { + return false + } + + if (exp.location?.column !== undefined && exp.location.column !== actualOffense.location.start.column) { + return false + } + + return true + }) + + if (matchIndex !== -1) { + unmatched.splice(matchIndex, 1) + const actualIndex = unmatchedActual.findIndex(o => o === actualOffense) + if (actualIndex !== -1) { + unmatchedActual.splice(actualIndex, 1) + } + } + } + + if (unmatched.length > 0 || unmatchedActual.length > 0) { + const errors: string[] = [] + + if (unmatched.length > 0) { + errors.push(`Expected ${severity}(s) not found:`) + unmatched.forEach(exp => { + const location = exp.location?.line !== undefined + ? exp.location?.column !== undefined + ? ` at ${exp.location.line}:${exp.location.column}` + : ` at line ${exp.location.line}` + : "" + errors.push(` - "${exp.message}"${location}`) + }) + } + + if (unmatchedActual.length > 0) { + errors.push(`Unexpected ${severity}(s) found:`) + unmatchedActual.forEach(offense => { + errors.push( + ` - "${offense.message}" at ${offense.location.start.line}:${offense.location.start.column}` + ) + }) + } + + throw new Error(errors.join("\n")) + } +} diff --git a/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts index 8bc529690..6b2824316 100644 --- a/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts +++ b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts @@ -1,75 +1,49 @@ import dedent from "dedent" -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { ERBCommentSyntax } from "../../src/rules/erb-comment-syntax.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("ERBCommentSyntax", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBCommentSyntax) +describe("ERBCommentSyntax", () => { test("when the ERB comment syntax is correct", () => { - const html = dedent` + expectNoOffenses(dedent` <%# good comment %> - ` - - const linter = new Linter(Herb, [ERBCommentSyntax]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(0) + `) }) test("when the ERB multi-line comment syntax is correct", () => { - const html = dedent` + expectNoOffenses(dedent` <% # good comment %> - ` - - const linter = new Linter(Herb, [ERBCommentSyntax]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(0) + `) }) test("when the ERB multi-line comment syntax is correct with multiple comment lines", () => { - const html = dedent` + expectNoOffenses(dedent` <% # good comment # good comment %> - ` - - const linter = new Linter(Herb, [ERBCommentSyntax]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(0) + `) }) test("when the ERB comment syntax is incorrect", () => { - const html = dedent` - <% # bad comment %> - ` + expectError("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") - const linter = new Linter(Herb, [ERBCommentSyntax]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") + assertOffenses(dedent` + <% # bad comment %> + `) }) test("when the ERB comment syntax is incorrect multiple times in one file", () => { - const html = dedent` + expectError("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") + expectError("Use `<%#` instead of `<%= #`. Ruby comments immediately after ERB tags can cause parsing issues.") + + assertOffenses(dedent` <% # first bad comment %> <%= # second bad comment %> - ` - - const linter = new Linter(Herb, [ERBCommentSyntax]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].message).toBe("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") - expect(lintResult.offenses[1].message).toBe("Use `<%#` instead of `<%= #`. Ruby comments immediately after ERB tags can cause parsing issues.") + `) }) }) diff --git a/javascript/packages/linter/test/rules/erb-no-empty-tags.test.ts b/javascript/packages/linter/test/rules/erb-no-empty-tags.test.ts index 0034528db..9f901aaa0 100644 --- a/javascript/packages/linter/test/rules/erb-no-empty-tags.test.ts +++ b/javascript/packages/linter/test/rules/erb-no-empty-tags.test.ts @@ -1,19 +1,13 @@ import dedent from "dedent" - -import { describe, test, expect, beforeAll } from "vitest" - -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" - +import { describe, test } from "vitest" import { ERBNoEmptyTagsRule } from "../../src/rules/erb-no-empty-tags.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("ERBNoEmptyTagsRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBNoEmptyTagsRule) +describe("ERBNoEmptyTagsRule", () => { test("should not report errors for valid ERB tags with content", () => { - const html = dedent` + expectNoOffenses(dedent`

    <%= title %>

    @@ -27,78 +21,50 @@ describe("ERBNoEmptyTagsRule", () => { <% end %> <%= "" %> - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("should not report errors for incomplete erb tags", () => { - const html = dedent` + expectNoOffenses(dedent` <% - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("should not report errors for incomplete erb output tags", () => { - const html = dedent` + expectNoOffenses(dedent` <%= - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("should report errors for completely empty ERB tags", () => { - const html = dedent` + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 2, column: 2 }) + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 3, column: 2 }) + + assertOffenses(dedent`

    <% %> <%= %>

    - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") - expect(lintResult.offenses[1].message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") + `) }) test("should report errors for whitespace-only ERB tags", () => { - const html = dedent` + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 2, column: 2 }) + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 3, column: 2 }) + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 4, column: 2 }) + + assertOffenses(dedent`

    <% %> <%= %> <% %>

    - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(3) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(3) - lintResult.offenses.forEach(message => { - expect(message.message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") - }) + `) }) test("should not report errors for ERB tags with meaningful content", () => { - const html = dedent` + expectNoOffenses(dedent`
    <%= user.name %> <% if condition %> @@ -106,17 +72,14 @@ describe("ERBNoEmptyTagsRule", () => { <% # this is a comment %> <%= @variable %>
    - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("should handle mixed valid and invalid ERB tags", () => { - const html = dedent` + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 3, column: 2 }) + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 5, column: 2 }) + + assertOffenses(dedent`
    <%= user.name %> <% %> @@ -124,38 +87,18 @@ describe("ERBNoEmptyTagsRule", () => { <%= %> <% end %>
    - ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - - lintResult.offenses.forEach(message => { - expect(message.message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") - }) + `) }) test("should handle empty ERB tag in attribute value", () => { - const html = `
    ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 1, column: 12 }) + + assertOffenses(`
    `) }) test("should handle empty ERB tag in open tag", () => { - const html = `
    >
    ` - const linter = new Linter(Herb, [ERBNoEmptyTagsRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.") + expectError("ERB tag should not be empty. Remove empty ERB tags or add content.", { line: 1, column: 5 }) + + assertOffenses(`
    >
    `) }) }) diff --git a/javascript/packages/linter/test/rules/erb-no-output-control-flow.test.ts b/javascript/packages/linter/test/rules/erb-no-output-control-flow.test.ts index 2a6777e7c..0091b7ca5 100644 --- a/javascript/packages/linter/test/rules/erb-no-output-control-flow.test.ts +++ b/javascript/packages/linter/test/rules/erb-no-output-control-flow.test.ts @@ -1,15 +1,12 @@ -import { describe, it, beforeAll, expect } from "vitest" -import { Herb } from "@herb-tools/node-wasm" +import { describe, it } from "vitest" import dedent from "dedent"; -import { Linter } from "../../src/linter"; import { ERBNoOutputControlFlowRule } from "../../src/rules/erb-no-output-control-flow"; +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("erb-no-output-control-flow", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBNoOutputControlFlowRule) +describe("erb-no-output-control-flow", () => { it("should allow if statements without output tags", () => { const html = dedent` <% if true %> @@ -20,12 +17,8 @@ describe("erb-no-output-control-flow", () => {
    Text3
    <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + + expectNoOffenses(html) }) it("should not allow if statments with output tags", () => { @@ -34,13 +27,9 @@ describe("erb-no-output-control-flow", () => {
    Text1
    <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + expectError("Control flow statements like `if` should not be used with output tags. Use `<% if ... %>` instead.") + assertOffenses(html) }) it("should not allow unless statements with output tags", () => { @@ -49,13 +38,9 @@ describe("erb-no-output-control-flow", () => {
    Text1
    <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + expectError("Control flow statements like `unless` should not be used with output tags. Use `<% unless ... %>` instead.") + assertOffenses(html) }) it("should not allow end statements with output tags", () => { @@ -64,13 +49,9 @@ describe("erb-no-output-control-flow", () => {
    Text1
    <%= end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + expectError("Control flow statements like `end` should not be used with output tags. Use `<% end ... %>` instead.") + assertOffenses(html) }) it("should not allow nested control flow blocks with output tags", () => { @@ -82,13 +63,9 @@ describe("erb-no-output-control-flow", () => { <% end %> <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + expectError("Control flow statements like `if` should not be used with output tags. Use `<% if ... %>` instead.") + assertOffenses(html) }) it('should show multiple errors for multiple output tags', () => { @@ -101,13 +78,11 @@ describe("erb-no-output-control-flow", () => {
    Text3
    <%= end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(3) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(3) + expectError("Control flow statements like `if` should not be used with output tags. Use `<% if ... %>` instead.") + expectError("Control flow statements like `else` should not be used with output tags. Use `<% else ... %>` instead.") + expectError("Control flow statements like `end` should not be used with output tags. Use `<% end ... %>` instead.") + assertOffenses(html) }) it("should show an error for outputting control flow blocks with nested control flow blocks", () => { @@ -118,13 +93,9 @@ describe("erb-no-output-control-flow", () => { <% end %> <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + expectError("Control flow statements like `if` should not be used with output tags. Use `<% if ... %>` instead.") + assertOffenses(html) }) it("should not report for link to with an if condition", () => { @@ -133,13 +104,8 @@ describe("erb-no-output-control-flow", () => { Click <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should not report on form_builder.fieldset with block", () => { @@ -158,25 +124,15 @@ describe("erb-no-output-control-flow", () => { <%# ... %> <% end %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should not report on yield with if in the same ERB tag", () => { const html = dedent` <%= yield(:header) if content_for?(:header) %> ` - - const linter = new Linter(Herb, [ERBNoOutputControlFlowRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) }) diff --git a/javascript/packages/linter/test/rules/erb-no-silent-tag-in-attribute-name.test.ts b/javascript/packages/linter/test/rules/erb-no-silent-tag-in-attribute-name.test.ts index c79738655..dbe71de41 100644 --- a/javascript/packages/linter/test/rules/erb-no-silent-tag-in-attribute-name.test.ts +++ b/javascript/packages/linter/test/rules/erb-no-silent-tag-in-attribute-name.test.ts @@ -1,16 +1,13 @@ import dedent from "dedent" -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { ERBNoSilentTagInAttributeNameRule } from "../../src/rules/erb-no-silent-tag-in-attribute-name.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("ERBNoSilentTagInAttributeNameRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBNoSilentTagInAttributeNameRule) +describe("ERBNoSilentTagInAttributeNameRule", () => { test("valid attributes with output ERB tags", () => { const html = dedent`
    -target="value">
    @@ -18,12 +15,7 @@ describe("ERBNoSilentTagInAttributeNameRule", () => { -field="text"> ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("valid static attribute names", () => { @@ -32,12 +24,8 @@ describe("ERBNoSilentTagInAttributeNameRule", () => { Logo ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("valid conditional attributes with ERB control flow", () => { @@ -46,68 +34,45 @@ describe("ERBNoSilentTagInAttributeNameRule", () => { class="admin"<% end %>> enabled="true"<% end %>> ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("invalid attribute with silent ERB tag", () => { const html = dedent`
    -target="value">
    ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-no-silent-tag-in-attribute-name") - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) test("invalid attribute with trimming silent ERB tag", () => { const html = dedent`
    -id="thing">
    ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-no-silent-tag-in-attribute-name") - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%-`) do not output content and should not be used in attribute names.") + + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%-`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) test("invalid attribute with comment ERB tag", () => { const html = dedent`
    -target="value">
    ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-no-silent-tag-in-attribute-name") - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%#`) do not output content and should not be used in attribute names.") + + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%#`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) test("multiple invalid attributes in same element", () => { const html = dedent`
    -target="value" id-<% another %>-suffix="test">
    ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") - expect(lintResult.offenses[1].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) test("mixed valid and invalid ERB tags in different attributes", () => { @@ -118,14 +83,9 @@ describe("ERBNoSilentTagInAttributeNameRule", () => { class="<%= valid_class %>" >

    ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-no-silent-tag-in-attribute-name") - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) test("nested HTML elements with various ERB patterns", () => { @@ -136,14 +96,9 @@ describe("ERBNoSilentTagInAttributeNameRule", () => { ` - const linter = new Linter(Herb, [ERBNoSilentTagInAttributeNameRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") - expect(lintResult.offenses[1].message).toBe("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%#`) do not output content and should not be used in attribute names.") + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%`) do not output content and should not be used in attribute names.") + expectError("Remove silent ERB tag from HTML attribute name. Silent ERB tags (`<%#`) do not output content and should not be used in attribute names.") + assertOffenses(html) }) }) diff --git a/javascript/packages/linter/test/rules/erb-prefer-image-tag-helper.test.ts b/javascript/packages/linter/test/rules/erb-prefer-image-tag-helper.test.ts index acdb5d7e2..abe7e70e1 100644 --- a/javascript/packages/linter/test/rules/erb-prefer-image-tag-helper.test.ts +++ b/javascript/packages/linter/test/rules/erb-prefer-image-tag-helper.test.ts @@ -1,328 +1,134 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" import dedent from "dedent" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { ERBPreferImageTagHelperRule } from "../../src/rules/erb-prefer-image-tag-helper.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("erb-prefer-image-tag-helper", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectWarning, assertOffenses } = createLinterTest(ERBPreferImageTagHelperRule) +describe("erb-prefer-image-tag-helper", () => { test("passes for regular img tags without ERB", () => { - const html = 'Company logo' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('Company logo') }) test("passes for image_tag helper usage", () => { - const html = '<%= image_tag "logo.png", alt: "Company logo", class: "logo" %>' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('<%= image_tag "logo.png", alt: "Company logo", class: "logo" %>') }) test("fails for img with image_path helper", () => { - const html = '" alt="Logo">' + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag image_path("logo.png"), alt: "..." %>` instead.') - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag image_path("logo.png"), alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Logo">') }) test("fails for img with asset_path helper", () => { - const html = '" alt="Banner">' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag asset_path("banner.jpg"), alt: "..." %>` instead.') - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag asset_path("banner.jpg"), alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Banner">') }) test("handles self-closing img tags with image_path", () => { - const html = '" alt="Logo" />' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag image_path("logo.png"), alt: "..." %>` instead.') - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag image_path("logo.png"), alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Logo" />') }) test("ignores non-img tags with image_path", () => { - const html = '
    ">Content
    ' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('
    ">Content
    ') }) test("fails for img with Rails URL helpers", () => { - const html = 'Logo' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{Rails.application.routes.url_helpers.root_url}/icon.png", alt: "..." %>` instead.') - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{Rails.application.routes.url_helpers.root_url}/icon.png", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('Logo') }) test("fails for img with root_url helper", () => { - const html = 'Banner' + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_url}/banner.jpg", alt: "..." %>` instead.') - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_url}/banner.jpg", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('Banner') }) test("fails for img with custom path helpers", () => { - const html = 'Admin icon' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{admin_path}/icon.png", alt: "..." %>` instead.') - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{admin_path}/icon.png", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('Admin icon') }) test("fails for img with dynamic user avatar URL", () => { - const html = 'User avatar' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag user.avatar.url, alt: "..." %>` instead.') - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag user.avatar.url, alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('User avatar') }) test("fails for img with dynamic product image", () => { - const html = 'Product image' + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag product.image, alt: "..." %>` instead.') - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag product.image, alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('Product image') }) test("fails for img with any ERB expression", () => { - const html = 'Company logo' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag @company.logo_path, alt: "..." %>` instead.') - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag @company.logo_path, alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('Company logo') }) test("fails for img with multiple ERB expressions", () => { - const html = '" alt="Logo">' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{base_url}#{image_path("logo.png")}", alt: "..." %>` instead.') - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{base_url}#{image_path("logo.png")}", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Logo">') }) test("fails for img with ERB expression containing string literal", () => { - const html = '" alt="Icon">' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_path}#{"icon.png"}", alt: "..." %>` instead.') - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_path}#{"icon.png"}", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Icon">') }) test("fails for img with ERB expression containing string literal followed by another ERB tag", () => { - const html = '" alt="Icon">' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses).toHaveLength(1) + expectWarning('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_path}/assets/#{"icon.png"}", alt: "..." %>` instead.') - expect(lintResult.offenses[0].rule).toBe("erb-prefer-image-tag-helper") - expect(lintResult.offenses[0].message).toBe('Prefer `image_tag` helper over manual `` with dynamic ERB expressions. Use `<%= image_tag "#{root_path}/assets/#{"icon.png"}", alt: "..." %>` instead.') - expect(lintResult.offenses[0].severity).toBe("warning") + assertOffenses('" alt="Icon">') }) test("shouldn't flag empty src attribute", () => { - const html = 'EmptyEmpty' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('EmptyEmpty') }) test("passes for img tags with static paths only", () => { - const html = dedent` + expectNoOffenses(dedent`
    Logo External image Relative path
    - ` - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("passes for data URIs with embedded ERB content", () => { - const html = 'Logo' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('Logo') }) test("passes for mixed static and ERB content in src", () => { - const html = 'User avatar' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('User avatar') }) test("passes for data URI with SVG and embedded ERB", () => { - const html = 'SVG' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('SVG') }) test("passes for data URI with PNG", () => { - const html = '1x1 transparent image' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('1x1 transparent image') }) test("passes for data URI with PNG with ERB", () => { - const html = '" alt="1x1 transparent image">' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('" alt="1x1 transparent image">') }) test("passes for img with only https URL", () => { - const html = 'External image' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('External image') }) test("passes for img with only http URL", () => { - const html = 'External image' - - const linter = new Linter(Herb, [ERBPreferImageTagHelperRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('External image') }) }) diff --git a/javascript/packages/linter/test/rules/erb-require-whitespace-inside-tags.test.ts b/javascript/packages/linter/test/rules/erb-require-whitespace-inside-tags.test.ts index 60037d6a7..0b54e3297 100644 --- a/javascript/packages/linter/test/rules/erb-require-whitespace-inside-tags.test.ts +++ b/javascript/packages/linter/test/rules/erb-require-whitespace-inside-tags.test.ts @@ -1,15 +1,12 @@ -import { describe, it, beforeAll, expect } from "vitest" -import { Herb } from "@herb-tools/node-wasm" +import { describe, it } from "vitest" import dedent from "dedent"; -import { Linter } from "../../src/linter"; import { ERBRequireWhitespaceRule } from "../../src/rules/erb-require-whitespace-inside-tags"; +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("erb-require-whitespace-inside-tags", () => { +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBRequireWhitespaceRule) - beforeAll(async () => { - await Herb.load() - }) +describe("erb-require-whitespace-inside-tags", () => { it("should not report for correct whitespace in ERB tags", () => { const html = dedent` @@ -17,12 +14,8 @@ describe("erb-require-whitespace-inside-tags", () => { Hello, admin. <% end %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should require a space after <% and before %> in ERB tags", () => { @@ -30,13 +23,10 @@ describe("erb-require-whitespace-inside-tags", () => { <%if true%> <% end %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) + expectError("Add whitespace after `<%`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should require a space after <%= and before %> in ERB output tags", () => { @@ -44,13 +34,10 @@ describe("erb-require-whitespace-inside-tags", () => { <%=user.name%> <%= user.name %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) + expectError("Add whitespace after `<%=`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should report errors for only missing opening whitespace", () => { @@ -59,12 +46,9 @@ describe("erb-require-whitespace-inside-tags", () => { Hello, user. <% end %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toMatch(/Add whitespace after/i) + expectError("Add whitespace after `<%`.") + assertOffenses(html) }) it("should report errors for only missing closing whitespace", () => { @@ -73,12 +57,9 @@ describe("erb-require-whitespace-inside-tags", () => { Hello, admin. <% end %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toMatch(/Add whitespace before/i) + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should report multiple errors for multiple offenses", () => { @@ -88,12 +69,14 @@ describe("erb-require-whitespace-inside-tags", () => { Hello, admin. <%end%> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(6) - expect(lintResult.offenses).toHaveLength(6) + expectError("Add whitespace after `<%=`.") + expectError("Add whitespace before `%>`.") + expectError("Add whitespace after `<%`.") + expectError("Add whitespace before `%>`.") + expectError("Add whitespace after `<%`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should not report for non-ERB content", () => { @@ -101,12 +84,8 @@ describe("erb-require-whitespace-inside-tags", () => {
    Hello

    World

    ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should handle mixed correct and incorrect ERB tags", () => { @@ -117,12 +96,12 @@ describe("erb-require-whitespace-inside-tags", () => {

    Hello, admin.

    <%end%> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(4) - expect(lintResult.offenses).toHaveLength(4) + expectError("Add whitespace after `<%=`.") + expectError("Add whitespace before `%>`.") + expectError("Add whitespace after `<%`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should handle empty erb tags", () => { @@ -134,12 +113,8 @@ describe("erb-require-whitespace-inside-tags", () => { %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should require whitespace after # in ERB comment tags", () => { @@ -148,54 +123,36 @@ describe("erb-require-whitespace-inside-tags", () => { <%#This is a comment without spaces%> <%# %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses).toHaveLength(2) - - expect(lintResult.offenses[0].message).toBe("Add whitespace after `<%#`.") - expect(lintResult.offenses[1].message).toBe("Add whitespace before `%>`.") + expectError("Add whitespace after `<%#`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should not report ERB comment tags with equals signs", () => { const html = dedent` <%#= link_to "New watch list", new_watch_list_path, class: "btn btn-ghost" %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should report ERB comment tags with equals sign and no space after", () => { const html = dedent` <%#=link_to "New watch list", new_watch_list_path, class: "btn btn-ghost"%> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].message).toBe("Add whitespace after `<%#=`.") - expect(lintResult.offenses[1].message).toBe("Add whitespace before `%>`.") + expectError("Add whitespace after `<%#=`.") + expectError("Add whitespace before `%>`.") + assertOffenses(html) }) it("should not report ERB comment tags with equals followed by space", () => { const html = dedent` <%# = link_to "New watch list", new_watch_list_path, class: "btn btn-ghost" %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("should handle multi-line ERB comment tags", () => { @@ -211,11 +168,7 @@ describe("erb-require-whitespace-inside-tags", () => { class: "btn btn-ghost" %> ` - - const linter = new Linter(Herb, [ERBRequireWhitespaceRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) }) diff --git a/javascript/packages/linter/test/rules/erb-requires-trailing-newline.test.ts b/javascript/packages/linter/test/rules/erb-requires-trailing-newline.test.ts index 5dc5c439d..63c149b01 100644 --- a/javascript/packages/linter/test/rules/erb-requires-trailing-newline.test.ts +++ b/javascript/packages/linter/test/rules/erb-requires-trailing-newline.test.ts @@ -1,16 +1,13 @@ import dedent from "dedent" -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { ERBRequiresTrailingNewlineRule } from "../../src/rules/erb-requires-trailing-newline.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("ERBRequiresTrailingNewlineRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBRequiresTrailingNewlineRule) +describe("ERBRequiresTrailingNewlineRule", () => { test("should not report errors for files ending with a trailing newline", () => { const html = dedent`

    @@ -18,22 +15,13 @@ describe("ERBRequiresTrailingNewlineRule", () => {

    ` + '\n' - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should not report errors for single newline files", () => { const html = "\n" - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should not report errors for files with multiple lines ending with newline", () => { @@ -42,12 +30,7 @@ describe("ERBRequiresTrailingNewlineRule", () => { <%= render partial: "footer" %> ` + '\n' - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should report errors for files without trailing newline", () => { @@ -57,26 +40,15 @@ describe("ERBRequiresTrailingNewlineRule", () => { ` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "test.html.erb" }) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "test.html.erb" }) }) test("should report errors for single line files without newline", () => { const html = `<%= render partial: "header" %>` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "test.html.erb" }) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "test.html.erb" }) }) test("should handle files with mixed content without trailing newline", () => { @@ -89,14 +61,8 @@ describe("ERBRequiresTrailingNewlineRule", () => { ` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "test.html.erb" }) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "test.html.erb" }) }) test("should handle files with mixed content with trailing newline", () => { @@ -109,56 +75,33 @@ describe("ERBRequiresTrailingNewlineRule", () => { ` + '\n' - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should handle ERB-only template without trailing newline", () => { const html = `<%= hello world %>` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "test.html.erb" }) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "test.html.erb" }) }) test("should handle ERB-only template with trailing newline", () => { const html = `<%= hello world %>` + '\n' - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should not flag empty file", () => { const html = `` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should flag empty file with whitespace", () => { const html = ` ` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "test.html.erb" }) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "test.html.erb" }) }) test("should not flag snippets without trailing newline (fileName: null)", () => { @@ -168,42 +111,25 @@ describe("ERBRequiresTrailingNewlineRule", () => { ` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: null }) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html, { fileName: null }) }) test("should not flag single line snippets without trailing newline (fileName: null)", () => { const html = `<%= render partial: "header" %>` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: null }) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html, { fileName: null }) }) test("should not flag single line snippets without trailing newline (fileName: undefined)", () => { const html = `<%= render partial: "header" %>` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: undefined }) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html, { fileName: undefined }) }) test("should not flag single line snippets without trailing newline with no context", () => { const html = `<%= render partial: "header" %>` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html) - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) test("should flag files without trailing newline when fileName is provided", () => { @@ -213,13 +139,7 @@ describe("ERBRequiresTrailingNewlineRule", () => { ` - const linter = new Linter(Herb, [ERBRequiresTrailingNewlineRule]) - const lintResult = linter.lint(html, { fileName: "template.html.erb" }) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].rule).toBe("erb-requires-trailing-newline") - expect(lintResult.offenses[0].message).toBe("File must end with trailing newline") + expectError("File must end with trailing newline") + assertOffenses(html, { fileName: "template.html.erb" }) }) }) diff --git a/javascript/packages/linter/test/rules/erb-right-trim.test.ts b/javascript/packages/linter/test/rules/erb-right-trim.test.ts index 3e97321a4..958d6ff51 100644 --- a/javascript/packages/linter/test/rules/erb-right-trim.test.ts +++ b/javascript/packages/linter/test/rules/erb-right-trim.test.ts @@ -1,147 +1,119 @@ import dedent from "dedent" -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" - +import { describe, test } from "vitest" import { ERBRightTrimRule } from "../../src/rules/erb-right-trim.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("ERBRightTrimRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(ERBRightTrimRule) +describe("ERBRightTrimRule", () => { test("when the erb tag close with %>", () => { - const html = dedent` + expectNoOffenses(dedent`

    <%= title %>

    - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.offenses).toHaveLength(0) + `) }) - test("when the erb tag close with -%>", () => { - const html = dedent` + test("when output erb tag closes with -%>", () => { + expectNoOffenses(dedent`

    <%= title -%>

    - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) + `) + }) - expect(lintResult.offenses).toHaveLength(0) + test("when non-output tag uses -%>", () => { + expectError("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + + assertOffenses(dedent` + <% if condition -%> +

    Content

    + <% elsif other_condition -%> +

    Content

    + <% elsif yet_another_condition -%> +

    Content

    + <% else -%> +

    Content

    + <% end -%> + `) }) test("when the erb tag close with =%>", () => { - const html = dedent` + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + + assertOffenses(dedent`

    <%= title =%> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-right-trim") - expect(lintResult.offenses[0].message).toBe("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + `) }) test("when an if block uses =%>", () => { - const html = dedent` + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + + assertOffenses(dedent` <% if condition =%>

    Content

    - <% end %> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-right-trim") - expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + <% end =%> + `) }) - test("when an if-else block uses =%>", () => { - const html = dedent` - <% if condition =%> -

    True branch

    - <% else =%> -

    False branch

    - <% end %> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - expect(lintResult.offenses[0].code).toBe("erb-right-trim") - - expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[1].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") - }) + test("when a loop uses =%>", () => { + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") - test("when each block uses =%>", () => { - const html = dedent` + assertOffenses(dedent` <% items.each do |item| =%>
  • <%= item %>
  • <% end %> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("erb-right-trim") - expect(lintResult.offenses[0].message).toBe("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + `) }) - test("when non-output tag uses -%>", () => { - const html = dedent` - <% if condition -%> -

    Content

    - <% elsif other_condition -%> -

    Content

    - <% elsif yet_another_condition -%> -

    Content

    - <% else -%> -

    Content

    - <% end -%> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(5) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(5) - - expect(lintResult.offenses[0].code).toBe("erb-right-trim") - expect(lintResult.offenses[0].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[1].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[2].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[3].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[4].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + test("when multiple lines use =%>", () => { + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + + assertOffenses(dedent` + <%= first =%> + <%= second =%> + <%= third =%> + `) }) - test("when multiple non-output tags use trimming", () => { - const html = dedent` - <% items.each do |item| -%> -
  • <%= item %>
  • - <% end -%> - ` - const linter = new Linter(Herb, [ERBRightTrimRule]) - const lintResult = linter.lint(html) + test("when mixed valid and invalid syntax is used", () => { + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") + expectError("Use `-%>` instead of `=%>` for right-trimming. The `=%>` syntax is obscure and not well-supported in most ERB engines") - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) + assertOffenses(dedent` + <%= valid %> + <%= invalid_trim =%> + <%= valid_trim -%> + <%= another_invalid =%> + `) + }) - expect(lintResult.offenses[0].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") - expect(lintResult.offenses[1].message).toBe("Right-trimming with `-%>` has no effect on non-output ERB tags. Use `%>` instead") + test("when silent ERB uses =%>", () => { + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + + assertOffenses(dedent` + <% silent_operation =%> + `) + }) + + test("handles =%> in nested structures", () => { + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + expectError("Right-trimming with `=%>` has no effect on non-output ERB tags. Use `%>` instead") + + assertOffenses(dedent` + <% if outer_condition =%> + <% if inner_condition =%> +

    Nested content

    + <% end %> + <% end %> + `) }) }) diff --git a/javascript/packages/linter/test/rules/html-anchor-require-href-rule.test.ts b/javascript/packages/linter/test/rules/html-anchor-require-href-rule.test.ts index 1ee7b3653..e26cd78f0 100644 --- a/javascript/packages/linter/test/rules/html-anchor-require-href-rule.test.ts +++ b/javascript/packages/linter/test/rules/html-anchor-require-href-rule.test.ts @@ -1,81 +1,38 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLAnchorRequireHrefRule } from "../../src/rules/html-anchor-require-href.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-anchor-require-href", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAnchorRequireHrefRule) +describe("html-anchor-require-href", () => { test("passes for a with href attribute", () => { - const html = 'My link' - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('My link') }) test("fails for a without href attribute", () => { - const html = "My link" - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) + expectError("Add an `href` attribute to `` to ensure it is focusable and accessible.") - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].rule).toBe("html-anchor-require-href") - expect(lintResult.offenses[0].message).toBe( - "Add an `href` attribute to `` to ensure it is focusable and accessible.", - ) - expect(lintResult.offenses[0].severity).toBe("error") + assertOffenses("My link") }) test("fails for multiple a tags without href", () => { - const html = "My linkMy other link" - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) + expectError("Add an `href` attribute to `` to ensure it is focusable and accessible.") + expectError("Add an `href` attribute to `` to ensure it is focusable and accessible.") - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) + assertOffenses("My linkMy other link") }) test("passes for img with ERB alt attribute", () => { - const html = 'My Link' - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses('My Link') }) test("ignores non-a tags", () => { - const html = "
    My div
    " - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses("
    My div
    ") }) test("handles mixed case a tags", () => { - const html = "My link" - - const linter = new Linter(Herb, [HTMLAnchorRequireHrefRule]) - const lintResult = linter.lint(html) + expectError("Add an `href` attribute to `` to ensure it is focusable and accessible.") - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe( - "Add an `href` attribute to `` to ensure it is focusable and accessible.", - ) + assertOffenses("My link") }) }) diff --git a/javascript/packages/linter/test/rules/html-aria-attribute-must-be-valid.test.ts b/javascript/packages/linter/test/rules/html-aria-attribute-must-be-valid.test.ts index a8c13cd34..3dc0666b3 100644 --- a/javascript/packages/linter/test/rules/html-aria-attribute-must-be-valid.test.ts +++ b/javascript/packages/linter/test/rules/html-aria-attribute-must-be-valid.test.ts @@ -1,76 +1,42 @@ import dedent from "dedent" -import { describe, it, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, it } from "vitest" import { HTMLAriaAttributeMustBeValid } from "../../src/rules/html-aria-attribute-must-be-valid.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-aria-attribute-must-be-valid", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAriaAttributeMustBeValid) +describe("html-aria-attribute-must-be-valid", () => { it("allows a div with a valid aria attribute", () => { const html = '
    ' - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("ignores non-aria attributes", () => { const html = '
    ' - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(html) }) it("fails when a div has an invalid aria attribute", () => { const html = '
    ' - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The attribute `aria-bogus` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.' - ) + expectError('The attribute `aria-bogus` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.') + assertOffenses(html) }) it("fails for mistyped aria name", () => { const html = '' - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The attribute `aria-lable` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.' - ) + expectError('The attribute `aria-lable` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.') + assertOffenses(html) }) it("fails for aria-", () => { const html = '' - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The attribute `aria-` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.' - ) + expectError('The attribute `aria-` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.') + assertOffenses(html) }) it("fails for aria-labelled-by", () => { @@ -79,15 +45,8 @@ describe("html-aria-attribute-must-be-valid", () => { I agree to the Terms and Conditions. ` - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The attribute `aria-labelled-by` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.' - ) + expectError('The attribute `aria-labelled-by` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.') + assertOffenses(html) }) it("fails for aria-described-by", () => { @@ -96,14 +55,7 @@ describe("html-aria-attribute-must-be-valid", () => {
    Password must be at least 8 characters
    ` - const linter = new Linter(Herb, [HTMLAriaAttributeMustBeValid]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The attribute `aria-described-by` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.' - ) + expectError('The attribute `aria-described-by` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.') + assertOffenses(html) }) }) diff --git a/javascript/packages/linter/test/rules/html-aria-label-is-well-formatted.test.ts b/javascript/packages/linter/test/rules/html-aria-label-is-well-formatted.test.ts index 0de4b1e8f..2fc2c147e 100644 --- a/javascript/packages/linter/test/rules/html-aria-label-is-well-formatted.test.ts +++ b/javascript/packages/linter/test/rules/html-aria-label-is-well-formatted.test.ts @@ -1,165 +1,78 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLAriaLabelIsWellFormattedRule } from "../../src/rules/html-aria-label-is-well-formatted.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-aria-label-is-well-formatted", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAriaLabelIsWellFormattedRule) +describe("html-aria-label-is-well-formatted", () => { test("passes for properly formatted aria-label", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(``) }) test("passes for aria-label with proper sentence case", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("fails for aria-label starting with lowercase", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].rule).toBe("html-aria-label-is-well-formatted") - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value text should be formatted like visual text. Use sentence case (capitalize the first letter).") + expectError("The `aria-label` attribute value text should be formatted like visual text. Use sentence case (capitalize the first letter).") + assertOffenses(``) }) test("fails for aria-label with line breaks", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + expectError("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + assertOffenses(``) }) test("fails for aria-label with carriage return", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + expectError("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + assertOffenses(``) }) test("fails for aria-label with HTML entity line breaks", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + expectError("The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.") + assertOffenses(``) }) test("fails for snake_case aria-label", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + expectError("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + assertOffenses(``) }) test("fails for kebab-case aria-label", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + expectError("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + assertOffenses(``) }) test("fails for camelCase aria-label", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + expectError("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + assertOffenses(``) }) test("passes for aria-label with spaces and proper formatting", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("passes for aria-label with numbers", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("ignores other attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("handles multiple elements", () => { - const html = ` + expectError("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + assertOffenses(` - ` - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.") + `) }) test("passes for aria-label with ERB content", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("passes for mixed case attribute name", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAriaLabelIsWellFormattedRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) }) diff --git a/javascript/packages/linter/test/rules/html-aria-level-must-be-valid.test.ts b/javascript/packages/linter/test/rules/html-aria-level-must-be-valid.test.ts index 23edbf179..70ce675a7 100644 --- a/javascript/packages/linter/test/rules/html-aria-level-must-be-valid.test.ts +++ b/javascript/packages/linter/test/rules/html-aria-level-must-be-valid.test.ts @@ -1,257 +1,141 @@ import dedent from "dedent" - -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" - +import { describe, test } from "vitest" import { HTMLAriaLevelMustBeValidRule } from "../../src/rules/html-aria-level-must-be-valid.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("HTMLAriaLevelMustBeValidRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAriaLevelMustBeValidRule) +describe("HTMLAriaLevelMustBeValidRule", () => { test("allows valid aria-level values 1-6", () => { - const html = dedent` + expectNoOffenses(dedent`
    Main
    Section
    Subsection
    Sub-subsection
    Deep heading
    Footnote
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("allows elements without aria-level attribute", () => { - const html = dedent` + expectNoOffenses(dedent`
    No aria-level

    Regular heading

    Regular div
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("flags negative aria-level values", () => { - const html = dedent` -
    Negative
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `-1`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("html-aria-level-must-be-valid") - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `-1`.', - ) + assertOffenses(dedent` +
    Negative
    + `) }) test("flags zero aria-level value", () => { - const html = dedent` -
    Main
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `0`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("html-aria-level-must-be-valid") - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `0`.', - ) + assertOffenses(dedent` +
    Main
    + `) }) test("flags aria-level values greater than 6", () => { - const html = dedent` -
    Too deep
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `7`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("html-aria-level-must-be-valid") - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `7`.', - ) + assertOffenses(dedent` +
    Too deep
    + `) }) test("flags non-numeric aria-level values", () => { - const html = dedent` -
    Invalid
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `foo`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("html-aria-level-must-be-valid") - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `foo`.', - ) + assertOffenses(dedent` +
    Invalid
    + `) }) test("flags multiple invalid aria-level values", () => { - const html = dedent` + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `-1`.') + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `0`.') + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `7`.') + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `foo`.') + + assertOffenses(dedent`
    Negative
    Zero
    Too deep
    Invalid
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(4) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(4) - - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `-1`.', - ) - expect(lintResult.offenses[1].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `0`.', - ) - expect(lintResult.offenses[2].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `7`.', - ) - expect(lintResult.offenses[3].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `foo`.', - ) + `) }) test("handles floating point numbers", () => { - const html = dedent` -
    Float
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `1.5`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got `1.5`.', - ) + assertOffenses(dedent` +
    Float
    + `) }) test("flags whitespace in aria-level values", () => { - const html = dedent` -
    Whitespace
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got ` 2 `.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - 'The `aria-level` attribute must be an integer between 1 and 6, got ` 2 `.', - ) + assertOffenses(dedent` +
    Whitespace
    + `) }) test("allows ERB expressions in aria-level values", () => { - const html = dedent` + expectNoOffenses(dedent`
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("allows mixed ERB expressions with no output in aria-level values", () => { - const html = dedent` + expectNoOffenses(dedent`
    Dynamic level
    Dynamic level
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("disallows mixed ERB expressions with no output in aria-level values", () => { - const html = dedent` -
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `-1`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe('The `aria-level` attribute must be an integer between 1 and 6, got `-1`.') + assertOffenses(dedent` +
    Dynamic level
    + `) }) test("disallows mixed ERB expressions with valid static value and dynamic ERB output", () => { - const html = dedent` -
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got `1` and the ERB expression `<%= @level %>`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe('The `aria-level` attribute must be an integer between 1 and 6, got `1` and the ERB expression `<%= @level %>`.') + assertOffenses(dedent` +
    Dynamic level
    + `) }) test.todo("allows mixed ERB expressions in aria-level values if both branches are valid", () => { - const html = dedent` + expectNoOffenses(dedent`
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test.todo("flags mixed ERB expressions in aria-level values if one branch is valid", () => { - const html = dedent` -
    Dynamic level
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, at least one branch has an invlid value: `-1`.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe('The `aria-level` attribute must be an integer between 1 and 6, at least one branch has an invlid value: `-1`.') // TODO: message can be tweaked + assertOffenses(dedent` +
    Dynamic level
    + `) }) test("flags empty aria-level attribute", () => { - const html = dedent` -
    Empty value
    - ` - const linter = new Linter(Herb, [HTMLAriaLevelMustBeValidRule]) - const lintResult = linter.lint(html) + expectError('The `aria-level` attribute must be an integer between 1 and 6, got an empty value.') - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].code).toBe("html-aria-level-must-be-valid") - expect(lintResult.offenses[0].message).toBe('The `aria-level` attribute must be an integer between 1 and 6, got an empty value.') + assertOffenses(dedent` +
    Empty value
    + `) }) }) diff --git a/javascript/packages/linter/test/rules/html-aria-role-heading-requires-level.test.ts b/javascript/packages/linter/test/rules/html-aria-role-heading-requires-level.test.ts index 7229c6d36..7ad3c0c14 100644 --- a/javascript/packages/linter/test/rules/html-aria-role-heading-requires-level.test.ts +++ b/javascript/packages/linter/test/rules/html-aria-role-heading-requires-level.test.ts @@ -1,36 +1,17 @@ -import { describe, it, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, it } from "vitest" import { HTMLAriaRoleHeadingRequiresLevelRule } from "../../src/rules/html-aria-role-heading-requires-level.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-aria-role-heading-requires-level", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAriaRoleHeadingRequiresLevelRule) +describe("html-aria-role-heading-requires-level", () => { it("allows a div with the proper heading", () => { - const html = '
    Section Title
    ' - - const linter = new Linter(Herb, [HTMLAriaRoleHeadingRequiresLevelRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('
    Section Title
    ') }) it("fails when role=heading is used without aria-level", () => { - const html = '
    Section Title
    ' - - - const linter = new Linter(Herb, [HTMLAriaRoleHeadingRequiresLevelRule]) - const lintResult = linter.lint(html) + expectError(`Element with \`role="heading"\` must have an \`aria-level\` attribute.`) - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - expect(lintResult.offenses[0].message).toBe( - `Element with \`role="heading"\` must have an \`aria-level\` attribute.` - ) + assertOffenses('
    Section Title
    ') }) }) diff --git a/javascript/packages/linter/test/rules/html-aria-role-must-be-valid.test.ts b/javascript/packages/linter/test/rules/html-aria-role-must-be-valid.test.ts index 79c6702cb..470ddad6d 100644 --- a/javascript/packages/linter/test/rules/html-aria-role-must-be-valid.test.ts +++ b/javascript/packages/linter/test/rules/html-aria-role-must-be-valid.test.ts @@ -1,55 +1,25 @@ -import { describe, it, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, it } from "vitest" import { HTMLAriaRoleMustBeValidRule } from "../../src/rules/html-aria-role-must-be-valid.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-aria-role-must-be-valid", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAriaRoleMustBeValidRule) +describe("html-aria-role-must-be-valid", () => { it("should not show an error for valid attributes", () => { - const html = '
    Click Me
    ' - const linter = new Linter(Herb, [HTMLAriaRoleMustBeValidRule]) - - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('
    Click Me
    ') }) it("should show an error for an invalid attrbute", () => { - const html = `
    ` - const linter = new Linter(Herb, [HTMLAriaRoleMustBeValidRule]) + expectError("The `role` attribute must be a valid ARIA role. Role `invalid-role` is not recognized.") - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe("The `role` attribute must be a valid ARIA role. Role `invalid-role` is not recognized.") - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) + assertOffenses(`
    `) }) it("should not show an error for ERB content", () => { - const html = `
    ` - const linter = new Linter(Herb, [HTMLAriaRoleMustBeValidRule]) - - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(`
    `) }) it("should not show an error for static and ERB content", () => { - const html = `
    ` - const linter = new Linter(Herb, [HTMLAriaRoleMustBeValidRule]) - - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(`
    `) }) }) diff --git a/javascript/packages/linter/test/rules/html-attribute-double-quotes.test.ts b/javascript/packages/linter/test/rules/html-attribute-double-quotes.test.ts index 2c5d13ce5..4ef25566f 100644 --- a/javascript/packages/linter/test/rules/html-attribute-double-quotes.test.ts +++ b/javascript/packages/linter/test/rules/html-attribute-double-quotes.test.ts @@ -1,152 +1,67 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLAttributeDoubleQuotesRule } from "../../src/rules/html-attribute-double-quotes.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-attribute-double-quotes", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectWarning, assertOffenses } = createLinterTest(HTMLAttributeDoubleQuotesRule) +describe("html-attribute-double-quotes", () => { test("passes for double quoted attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(``) }) test("fails for single quoted attributes", () => { - const html = "" - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(2) - expect(lintResult.offenses).toHaveLength(2) - - expect(lintResult.offenses[0].rule).toBe("html-attribute-double-quotes") - expect(lintResult.offenses[0].message).toBe('Attribute `type` uses single quotes. Prefer double quotes for HTML attribute values: `type="text"`.') - expect(lintResult.offenses[0].severity).toBe("warning") - - expect(lintResult.offenses[1].rule).toBe("html-attribute-double-quotes") - expect(lintResult.offenses[1].message).toBe('Attribute `value` uses single quotes. Prefer double quotes for HTML attribute values: `value="Username"`.') - expect(lintResult.offenses[1].severity).toBe("warning") + expectWarning('Attribute `type` uses single quotes. Prefer double quotes for HTML attribute values: `type="text"`.') + expectWarning('Attribute `value` uses single quotes. Prefer double quotes for HTML attribute values: `value="Username"`.') + assertOffenses(``) }) test("passes for mixed content with double quotes", () => { - const html = 'Profile' - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`Profile`) }) test("fails for mixed content with single quotes", () => { - const html = "Profile" - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(3) - expect(lintResult.offenses[0].message).toBe('Attribute `href` uses single quotes. Prefer double quotes for HTML attribute values: `href="/profile"`.') - expect(lintResult.offenses[1].message).toBe('Attribute `title` uses single quotes. Prefer double quotes for HTML attribute values: `title="User Profile"`.') - expect(lintResult.offenses[2].message).toBe('Attribute `data-controller` uses single quotes. Prefer double quotes for HTML attribute values: `data-controller="dropdown"`.') + expectWarning('Attribute `href` uses single quotes. Prefer double quotes for HTML attribute values: `href="/profile"`.') + expectWarning('Attribute `title` uses single quotes. Prefer double quotes for HTML attribute values: `title="User Profile"`.') + expectWarning('Attribute `data-controller` uses single quotes. Prefer double quotes for HTML attribute values: `data-controller="dropdown"`.') + assertOffenses(`Profile`) }) test("passes for unquoted attributes (handled by other rule)", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("passes for attributes without values", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("handles self-closing tags with single quotes", () => { - const html = "Description" - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(2) - expect(lintResult.offenses[0].message).toBe('Attribute `src` uses single quotes. Prefer double quotes for HTML attribute values: `src="/image.jpg"`.') - expect(lintResult.offenses[1].message).toBe('Attribute `alt` uses single quotes. Prefer double quotes for HTML attribute values: `alt="Description"`.') + expectWarning('Attribute `src` uses single quotes. Prefer double quotes for HTML attribute values: `src="/image.jpg"`.') + expectWarning('Attribute `alt` uses single quotes. Prefer double quotes for HTML attribute values: `alt="Description"`.') + assertOffenses(`Description`) }) test("handles ERB with single quoted attributes", () => { - const html = "
    Content
    " - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(2) - expect(lintResult.offenses[0].message).toBe('Attribute `data-controller` uses single quotes. Prefer double quotes for HTML attribute values: `data-controller="<%= controller_name %>"`.') - expect(lintResult.offenses[1].message).toBe('Attribute `data-action` uses single quotes. Prefer double quotes for HTML attribute values: `data-action="click->toggle#action"`.') + expectWarning('Attribute `data-controller` uses single quotes. Prefer double quotes for HTML attribute values: `data-controller="<%= controller_name %>"`.') + expectWarning('Attribute `data-action` uses single quotes. Prefer double quotes for HTML attribute values: `data-action="click->toggle#action"`.') + assertOffenses(`
    Content
    `) }) test("handles mixed ERB with single quoted attributes", () => { - const html = "
    Content
    " - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(1) - expect(lintResult.offenses[0].message).toBe('Attribute `class` uses single quotes. Prefer double quotes for HTML attribute values: `class="static <%= class_list %> another-static"`.') + expectWarning('Attribute `class` uses single quotes. Prefer double quotes for HTML attribute values: `class="static <%= class_list %> another-static"`.') + assertOffenses(`
    Content
    `) }) test("allows single quotes when value contains double quotes", () => { - const html = `
    ` - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(`
    `) }) test("allows single quotes when value contains double quotes and ERB content", () => { - const html = `
    ` - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(`
    `) }) test("still fails for single quotes when value has no double quotes", () => { - const html = "
    " - - const linter = new Linter(Herb, [HTMLAttributeDoubleQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(2) - expect(lintResult.offenses[0].message).toBe('Attribute `id` uses single quotes. Prefer double quotes for HTML attribute values: `id="hello"`.') - expect(lintResult.offenses[1].message).toBe('Attribute `class` uses single quotes. Prefer double quotes for HTML attribute values: `class="world"`.') + expectWarning('Attribute `id` uses single quotes. Prefer double quotes for HTML attribute values: `id="hello"`.') + expectWarning('Attribute `class` uses single quotes. Prefer double quotes for HTML attribute values: `class="world"`.') + assertOffenses(`
    `) }) }) diff --git a/javascript/packages/linter/test/rules/html-attribute-equals-spacing.test.ts b/javascript/packages/linter/test/rules/html-attribute-equals-spacing.test.ts index 54283b74e..a04c77769 100644 --- a/javascript/packages/linter/test/rules/html-attribute-equals-spacing.test.ts +++ b/javascript/packages/linter/test/rules/html-attribute-equals-spacing.test.ts @@ -1,180 +1,96 @@ import dedent from "dedent" - -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" - +import { describe, test } from "vitest" import { HTMLAttributeEqualsSpacingRule } from "../../src/rules/html-attribute-equals-spacing.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("HTMLAttributeEqualsSpacingRule", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAttributeEqualsSpacingRule) +describe("HTMLAttributeEqualsSpacingRule", () => { test("valid attributes with no spacing around equals", () => { - const html = dedent` + expectNoOffenses(dedent`
    Logo - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) test("attribute with space before equals", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + assertOffenses(dedent`
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].code).toBe("html-attribute-equals-spacing") - expect(lintResult.offenses[0].message).toBe("Remove whitespace before `=` in HTML attribute") + `) }) test("attribute with space after equals", () => { - const html = dedent` + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent` - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].code).toBe("html-attribute-equals-spacing") - expect(lintResult.offenses[0].message).toBe("Remove whitespace after `=` in HTML attribute") + `) }) test("attribute with spaces both before and after equals", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent` - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace before `=` in HTML attribute") - expect(lintResult.offenses[1].message).toBe("Remove whitespace after `=` in HTML attribute") + `) }) test("attribute value without quotes and spaces after equals", () => { - const html = dedent` + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent`
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace after `=` in HTML attribute") + `) }) test("attribute with static and dynamic ERB before equals", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + assertOffenses(dedent`
    ="value">
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace before `=` in HTML attribute") + `) }) test("attribute with ERB before equals", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + assertOffenses(dedent`
    ="value">
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace before `=` in HTML attribute") + `) }) test("attribute with static, dynamic ERB, static before equals", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + assertOffenses(dedent`
    -user ="value">
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace before `=` in HTML attribute") + `) }) test("attribute value with ERB and spaces after equals", () => { - const html = dedent` + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent`
    >
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace after `=` in HTML attribute") + `) }) test("attribute with static, dynamic ERB, static after equals", () => { - const html = dedent` + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent`
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(1) - - expect(lintResult.offenses[0].message).toBe("Remove whitespace after `=` in HTML attribute") + `) }) test("multiple attributes with various spacing issues", () => { - const html = dedent` + expectError("Remove whitespace before `=` in HTML attribute") + expectError("Remove whitespace after `=` in HTML attribute") + expectError("Remove whitespace before `=` in HTML attribute") + expectError("Remove whitespace after `=` in HTML attribute") + assertOffenses(dedent`
    - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(4) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(4) + `) }) test("attributes without values should not trigger rule", () => { - const html = dedent` + expectNoOffenses(dedent` - ` - const linter = new Linter(Herb, [HTMLAttributeEqualsSpacingRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + `) }) }) diff --git a/javascript/packages/linter/test/rules/html-attribute-values-require-quotes.test.ts b/javascript/packages/linter/test/rules/html-attribute-values-require-quotes.test.ts index 4a9d7de80..7d7a781b9 100644 --- a/javascript/packages/linter/test/rules/html-attribute-values-require-quotes.test.ts +++ b/javascript/packages/linter/test/rules/html-attribute-values-require-quotes.test.ts @@ -1,119 +1,53 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLAttributeValuesRequireQuotesRule } from "../../src/rules/html-attribute-values-require-quotes.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-attribute-values-require-quotes", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAttributeValuesRequireQuotesRule) +describe("html-attribute-values-require-quotes", () => { test("passes for quoted attribute values", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(`
    `) }) test("fails for unquoted attribute values", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) // Both id and class are unquoted - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - - expect(lintResult.offenses[0].rule).toBe("html-attribute-values-require-quotes") - expect(lintResult.offenses[0].severity).toBe("error") - - expect(lintResult.offenses[0].message).toBe('Attribute value should be quoted: `id="hello"`. Always wrap attribute values in quotes.') - expect(lintResult.offenses[1].message).toBe('Attribute value should be quoted: `class="container"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `id="hello"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `class="container"`. Always wrap attribute values in quotes.') + assertOffenses(`
    `) }) test("passes for single-quoted values", () => { - const html = "
    " - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`
    `) }) test("passes for boolean attributes without values", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("handles mixed quoted and unquoted", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) // Only name is unquoted - expect(lintResult.offenses[0].message).toBe('Attribute value should be quoted: `name="username"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `name="username"`. Always wrap attribute values in quotes.') + assertOffenses(``) }) test("handles ERB in quoted attributes", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`
    `) }) test("handles ERB in unquoted attributes", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses[0].message).toBe('Attribute value should be quoted: `class="<%= classes %>"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `class="<%= classes %>"`. Always wrap attribute values in quotes.') + assertOffenses(`
    `) }) test("handles self-closing tags", () => { - const html = 'Photo' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe('Attribute value should be quoted: `src="photo"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `src="photo"`. Always wrap attribute values in quotes.') + assertOffenses(`Photo`) }) test("handles complex attribute values", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe('Attribute value should be quoted: `title="User"`. Always wrap attribute values in quotes.') + expectError('Attribute value should be quoted: `title="User"`. Always wrap attribute values in quotes.') + assertOffenses(``) }) test("ignores closing tags", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAttributeValuesRequireQuotesRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`
    `) }) }) diff --git a/javascript/packages/linter/test/rules/html-avoid-both-disabled-and-aria-disabled.test.ts b/javascript/packages/linter/test/rules/html-avoid-both-disabled-and-aria-disabled.test.ts index 0892eda84..dc6a08692 100644 --- a/javascript/packages/linter/test/rules/html-avoid-both-disabled-and-aria-disabled.test.ts +++ b/javascript/packages/linter/test/rules/html-avoid-both-disabled-and-aria-disabled.test.ts @@ -1,170 +1,87 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLAvoidBothDisabledAndAriaDisabledRule } from "../../src/rules/html-avoid-both-disabled-and-aria-disabled.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-avoid-both-disabled-and-aria-disabled", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLAvoidBothDisabledAndAriaDisabledRule) +describe("html-avoid-both-disabled-and-aria-disabled", () => { test("passes for button with only disabled", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses(``) }) test("passes for button with only aria-disabled", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("passes for button with neither attribute", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) test("fails for button with both disabled and aria-disabled", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].rule).toBe("html-avoid-both-disabled-and-aria-disabled") - expect(lintResult.offenses[0].message).toBe("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("fails for input with both attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].rule).toBe("html-avoid-both-disabled-and-aria-disabled") + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("fails for textarea with both attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("fails for select with both attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("fails for fieldset with both attributes", () => { - const html = '
    ' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(`
    `) }) test("fails for option with both attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("fails for optgroup with both attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("ignores elements that don't support disabled attribute", () => { - const html = '
    Not a form element
    ' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`
    Not a form element
    `) }) test("ignores anchor elements with both attributes", () => { - const html = '
    Link' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(`Link`) }) test("handles mixed case attribute names", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("handles boolean disabled attribute", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(``) }) test("handles multiple form elements", () => { - const html = ` + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + expectError("aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.") + assertOffenses(` - ` - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses).toHaveLength(2) + `) }) test("passes when attributes have ERB content", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLAvoidBothDisabledAndAriaDisabledRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses(``) }) }) diff --git a/javascript/packages/linter/test/rules/html-boolean-attributes-no-value.test.ts b/javascript/packages/linter/test/rules/html-boolean-attributes-no-value.test.ts index f125acf33..bd771671a 100644 --- a/javascript/packages/linter/test/rules/html-boolean-attributes-no-value.test.ts +++ b/javascript/packages/linter/test/rules/html-boolean-attributes-no-value.test.ts @@ -1,144 +1,78 @@ -import { describe, test, expect, beforeAll } from "vitest" -import { Herb } from "@herb-tools/node-wasm" -import { Linter } from "../../src/linter.js" +import { describe, test } from "vitest" import { HTMLBooleanAttributesNoValueRule } from "../../src/rules/html-boolean-attributes-no-value.js" +import { createLinterTest } from "../helpers/linter-test-helper.js" -describe("html-boolean-attributes-no-value", () => { - beforeAll(async () => { - await Herb.load() - }) +const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLBooleanAttributesNoValueRule) +describe("html-boolean-attributes-no-value", () => { test("passes for boolean attributes without values", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(0) + expectNoOffenses('') }) test("fails for boolean attributes with explicit values", () => { - const html = '' + expectError('Boolean attribute `checked` should not have a value. Use `checked` instead of `checked="checked"`.') + expectError('Boolean attribute `disabled` should not have a value. Use `disabled` instead of `disabled="disabled"`.') - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.warnings).toBe(0) - expect(lintResult.offenses).toHaveLength(2) - - expect(lintResult.offenses[0].rule).toBe("html-boolean-attributes-no-value") - expect(lintResult.offenses[0].message).toBe('Boolean attribute `checked` should not have a value. Use `checked` instead of `checked="checked"`.') - expect(lintResult.offenses[0].severity).toBe("error") - - expect(lintResult.offenses[1].rule).toBe("html-boolean-attributes-no-value") - expect(lintResult.offenses[1].message).toBe('Boolean attribute `disabled` should not have a value. Use `disabled` instead of `disabled="disabled"`.') - expect(lintResult.offenses[1].severity).toBe("error") + assertOffenses('') }) test("fails for boolean attributes with true/false values", () => { - const html = '' + expectError('Boolean attribute `disabled` should not have a value. Use `disabled` instead of `disabled="true"`.') - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(1) - expect(lintResult.offenses[0].message).toBe('Boolean attribute `disabled` should not have a value. Use `disabled` instead of `disabled="true"`.') + assertOffenses('') }) test("passes for non-boolean attributes with values", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(0) - expect(lintResult.warnings).toBe(0) + expectNoOffenses('') }) test("handles multiple boolean attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) + expectError('Boolean attribute `multiple` should not have a value. Use `multiple` instead of `multiple="multiple"`.') + expectError('Boolean attribute `selected` should not have a value. Use `selected` instead of `selected="selected"`.') - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses[0].message).toBe('Boolean attribute `multiple` should not have a value. Use `multiple` instead of `multiple="multiple"`.') - expect(lintResult.offenses[1].message).toBe('Boolean attribute `selected` should not have a value. Use `selected` instead of `selected="selected"`.') + assertOffenses('') }) test("handles self-closing tags with boolean attributes", () => { - const html = '' - - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) + expectError('Boolean attribute `checked` should not have a value. Use `checked` instead of `checked="checked"`.') + expectError('Boolean attribute `required` should not have a value. Use `required` instead of `required="true"`.') - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses[0].message).toBe('Boolean attribute `checked` should not have a value. Use `checked` instead of `checked="checked"`.') - expect(lintResult.offenses[1].message).toBe('Boolean attribute `required` should not have a value. Use `required` instead of `required="true"`.') + assertOffenses('') }) test("handles case insensitive boolean attributes", () => { - const html = '' + expectError('Boolean attribute `CHECKED` should not have a value. Use `checked` instead of `CHECKED="CHECKED"`.') + expectError('Boolean attribute `DISABLED` should not have a value. Use `disabled` instead of `DISABLED="disabled"`.') - const linter = new Linter(Herb, [HTMLBooleanAttributesNoValueRule]) - const lintResult = linter.lint(html) - - expect(lintResult.errors).toBe(2) - expect(lintResult.offenses[0].message).toBe('Boolean attribute `CHECKED` should not have a value. Use `checked` instead of `CHECKED="CHECKED"`.') - expect(lintResult.offenses[1].message).toBe('Boolean attribute `DISABLED` should not have a value. Use `disabled` instead of `DISABLED="disabled"`.') + assertOffenses('') }) test("passes for video controls", () => { - const html = '