Skip to content

Commit 7c07d9d

Browse files
authored
Parser: Re-parse on unexpected ";" error for more accurate errors (marcoroth#902)
This pull request changes the way the parser handles the `unexpected ";"` errors. In marcoroth#824, we changed the `herb_extract_ruby_to_buffer_with_semicolons` method to only add semicolons when needed. This pull request reverts that. But instead of just outputting the `unexpected ";"` errors (as it was before marcoroth#824), we check if the `;` was actually part of the source file. If it wasn't part of the source file we re-parse this individual `ERBContentNode` to check for syntax errors in the raw source of that `ERBContentNode` instead, which shouldn't have the semicolon and thus shouldn't append the `unexpected ";"` errors. Resolves marcoroth#854 Resolves marcoroth#865 Resolves marcoroth#899
1 parent da67f5c commit 7c07d9d

32 files changed

+832
-152
lines changed

java/snapshots/test.ruby.txt

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

javascript/packages/browser/test/browser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("@herb-tools/browser", () => {
3434
const simpleHtml = '<div><%= "Hello World" %></div>'
3535
const ruby = Herb.extractRuby(simpleHtml)
3636
expect(ruby).toBeDefined()
37-
expect(ruby).toBe(' "Hello World" ')
37+
expect(ruby).toBe(' "Hello World" ; ')
3838
})
3939

4040
test("extractHTML() extracts HTML content", async () => {

javascript/packages/linter/test/rules/parser-no-errors.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ describe("ParserNoErrorsRule", () => {
5757

5858
test("should report Ruby parse errors in ERB tags", () => {
5959
expectError("expect_expression_after_operator: unexpected end-of-input; expected an expression after the operator (`RUBY_PARSE_ERROR`)")
60-
expectError("unexpected_token_close_context: unexpected end-of-input, assuming it is closing the parent top level context (`RUBY_PARSE_ERROR`)")
6160
assertOffenses(`<%= 1 + %>`)
6261
})
6362

javascript/packages/node-wasm/test/node-wasm.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("@herb-tools/node-wasm", () => {
3434
const simpleHtml = '<div><%= "Hello World" %></div>'
3535
const ruby = Herb.extractRuby(simpleHtml)
3636
expect(ruby).toBeDefined()
37-
expect(ruby).toBe(' "Hello World" ')
37+
expect(ruby).toBe(' "Hello World" ; ')
3838
})
3939

4040
test("extractHTML() extracts HTML content", async () => {

javascript/packages/node/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"./extension/libherb/parser_helpers.c",
3131
"./extension/libherb/parser_match_tags.c",
3232
"./extension/libherb/parser.c",
33+
"./extension/libherb/position.c",
3334
"./extension/libherb/pretty_print.c",
3435
"./extension/libherb/prism_helpers.c",
3536
"./extension/libherb/range.c",

javascript/packages/node/test/node.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("@herb-tools/node", () => {
3434
const simpleHtml = '<div><%= "Hello World" %></div>'
3535
const ruby = Herb.extractRuby(simpleHtml)
3636
expect(ruby).toBeDefined()
37-
expect(ruby).toBe(' "Hello World" ')
37+
expect(ruby).toBe(' "Hello World" ; ')
3838
})
3939

4040
test("extractHTML() extracts HTML content", async () => {

rust/tests/cli_commands_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn test_version_functions() {
1212
fn test_extract_ruby() {
1313
let source = "<div><%= name %></div>";
1414
let ruby = extract_ruby(source).unwrap();
15-
assert_eq!(ruby, " name ");
15+
assert_eq!(ruby, " name ; ");
1616
}
1717

1818
#[test]
@@ -32,7 +32,7 @@ fn test_extract_ruby_complex() {
3232
let ruby = extract_ruby(source).unwrap();
3333
assert_eq!(
3434
ruby,
35-
" \n users.each do |user| \n user.name \n end \n "
35+
" \n users.each do |user| ;\n user.name ; \n end ;\n "
3636
);
3737
}
3838

rust/tests/snapshots/snapshot_test__extract_ruby_output.snap

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/analyze.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,35 @@ void herb_analyze_parse_tree(AST_DOCUMENT_NODE_T* document, const char* source)
14091409
free(invalid_context);
14101410
}
14111411

1412+
static void parse_erb_content_errors(AST_NODE_T* erb_node, const char* source) {
1413+
if (!erb_node || erb_node->type != AST_ERB_CONTENT_NODE) { return; }
1414+
AST_ERB_CONTENT_NODE_T* content_node = (AST_ERB_CONTENT_NODE_T*) erb_node;
1415+
1416+
if (!content_node->content || !content_node->content->value) { return; }
1417+
1418+
const char* content = content_node->content->value;
1419+
if (strlen(content) == 0) { return; }
1420+
1421+
pm_parser_t parser;
1422+
pm_options_t options = { 0, .partial_script = true };
1423+
pm_parser_init(&parser, (const uint8_t*) content, strlen(content), &options);
1424+
1425+
pm_node_t* root = pm_parse(&parser);
1426+
1427+
const pm_diagnostic_t* error = (const pm_diagnostic_t*) parser.error_list.head;
1428+
1429+
if (error != NULL) {
1430+
RUBY_PARSE_ERROR_T* parse_error =
1431+
ruby_parse_error_from_prism_error_with_positions(error, erb_node->location.start, erb_node->location.end);
1432+
1433+
hb_array_append(erb_node->errors, parse_error);
1434+
}
1435+
1436+
pm_node_destroy(&parser, root);
1437+
pm_parser_free(&parser);
1438+
pm_options_free(&options);
1439+
}
1440+
14121441
void herb_analyze_parse_errors(AST_DOCUMENT_NODE_T* document, const char* source) {
14131442
char* extracted_ruby = herb_extract_ruby_with_semicolons(source);
14141443

@@ -1422,6 +1451,19 @@ void herb_analyze_parse_errors(AST_DOCUMENT_NODE_T* document, const char* source
14221451

14231452
for (const pm_diagnostic_t* error = (const pm_diagnostic_t*) parser.error_list.head; error != NULL;
14241453
error = (const pm_diagnostic_t*) error->node.next) {
1454+
size_t error_offset = (size_t) (error->location.start - parser.start);
1455+
1456+
if (strstr(error->message, "unexpected ';'") != NULL) {
1457+
if (error_offset < strlen(extracted_ruby) && extracted_ruby[error_offset] == ';') {
1458+
if (error_offset >= strlen(source) || source[error_offset] != ';') {
1459+
AST_NODE_T* erb_node = find_erb_content_at_offset(document, source, error_offset);
1460+
1461+
if (erb_node) { parse_erb_content_errors(erb_node, source); }
1462+
1463+
continue;
1464+
}
1465+
}
1466+
}
14251467

14261468
RUBY_PARSE_ERROR_T* parse_error = ruby_parse_error_from_prism_error(error, (AST_NODE_T*) document, source, &parser);
14271469
hb_array_append(document->base.errors, parse_error);

src/ast_node.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#include "include/ast_node.h"
22
#include "include/ast_nodes.h"
33
#include "include/errors.h"
4+
#include "include/position.h"
45
#include "include/token.h"
56
#include "include/util.h"
7+
#include "include/visitor.h"
68

79
#include <prism.h>
810
#include <stdio.h>
@@ -76,3 +78,30 @@ void ast_node_set_positions_from_token(AST_NODE_T* node, const token_T* token) {
7678
bool ast_node_is(const AST_NODE_T* node, const ast_node_type_T type) {
7779
return node->type == type;
7880
}
81+
82+
typedef struct {
83+
position_T position;
84+
AST_NODE_T* found_node;
85+
} find_erb_at_position_context_T;
86+
87+
static bool find_erb_at_position_visitor(const AST_NODE_T* node, void* data) {
88+
find_erb_at_position_context_T* context = (find_erb_at_position_context_T*) data;
89+
90+
if (node->type == AST_ERB_CONTENT_NODE) {
91+
if (position_is_within_range(context->position, node->location.start, node->location.end)) {
92+
context->found_node = (AST_NODE_T*) node;
93+
return false;
94+
}
95+
}
96+
97+
return true;
98+
}
99+
100+
AST_NODE_T* find_erb_content_at_offset(AST_DOCUMENT_NODE_T* document, const char* source, size_t offset) {
101+
position_T position = position_from_source_with_offset(source, offset);
102+
find_erb_at_position_context_T context = { .position = position, .found_node = NULL };
103+
104+
herb_visit_node((AST_NODE_T*) document, find_erb_at_position_visitor, &context);
105+
106+
return context.found_node;
107+
}

0 commit comments

Comments
 (0)