Skip to content

Commit 18a9956

Browse files
authored
Parser: Introduce ERBStrictLocalsNode (#1424)
This pull request adds support for parsing and analyzing Rails strict locals declarations in ERB templates and representing them as structured `ERBStrictLocalsNode` nodes with individual `RubyStrictLocalNode` children. A new `strict_locals` parser option enables this analysis (defaults to false). When enabled, `ERBContentNodes` matching the `<%# locals: (...) %>` pattern are transformed into `ERBStrictLocalsNode` with fully extracted local variable information. For example, the following template: ```erb <%# locals: (user:, theme: "light", **attrs) %> ``` Produces the following with `strict_locals: true`: ```js @ DocumentNode (location: (1:0)-(1:47)) └── children: (1 item) └── @ ERBStrictLocalsNode (location: (1:0)-(1:47)) ├── tag_opening: "<%#" (location: (1:0)-(1:3)) ├── content: " locals: (user:, theme: "light", **attrs) " (location: (1:3)-(1:45)) ├── tag_closing: "%>" (location: (1:45)-(1:47)) └── locals: (3 items) ├── @ RubyStrictLocalNode (location: (1:13)-(1:18)) │ ├── name: "user" (location: (1:13)-(1:18)) │ ├── default_value: ∅ │ ├── required: true │ └── double_splat: false │ ├── @ RubyStrictLocalNode (location: (1:20)-(1:34)) │ ├── name: "theme" (location: (1:20)-(1:26)) │ ├── default_value: │ │ └── @ RubyLiteralNode (location: (1:27)-(1:34)) │ │ └── content: "\"light\"" │ │ │ ├── required: false │ └── double_splat: false │ └── @ RubyStrictLocalNode (location: (1:38)-(1:43)) ├── name: "attrs" (location: (1:38)-(1:43)) ├── default_value: ∅ ├── required: false └── double_splat: true ```
1 parent efc43b1 commit 18a9956

File tree

69 files changed

+2482
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2482
-25
lines changed

config.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,54 @@ errors:
410410
- name: layout
411411
type: string
412412
413+
- name: StrictLocalsPositionalArgumentError
414+
message:
415+
template: "Strict locals only support keyword arguments. Positional argument `%s` is not allowed. Use keyword argument format: `%s:`."
416+
arguments:
417+
- name
418+
- name
419+
420+
fields:
421+
- name: name
422+
type: string
423+
424+
- name: StrictLocalsBlockArgumentError
425+
message:
426+
template: "Strict locals only support keyword arguments. Block argument `&%s` is not allowed."
427+
arguments:
428+
- name
429+
430+
fields:
431+
- name: name
432+
type: string
433+
434+
- name: StrictLocalsSplatArgumentError
435+
message:
436+
template: "Strict locals only support keyword arguments. Splat argument `*%s` is not allowed."
437+
arguments:
438+
- name
439+
440+
fields:
441+
- name: name
442+
type: string
443+
444+
- name: StrictLocalsMissingParenthesisError
445+
message:
446+
template: "Strict locals declaration requires parentheses. Expected `locals: (...)` but got `locals: %s`."
447+
arguments:
448+
- rest
449+
450+
fields:
451+
- name: rest
452+
type: string
453+
454+
- name: StrictLocalsDuplicateDeclarationError
455+
message:
456+
template: "Duplicate strict locals declaration. Only the first `<%# locals: (...) %>` declaration is used by Rails."
457+
arguments: []
458+
459+
fields: []
460+
413461
warnings:
414462
fields: []
415463
types: []
@@ -1169,6 +1217,43 @@ nodes:
11691217
kind:
11701218
- RubyRenderLocalNode
11711219

1220+
- name: RubyStrictLocalNode
1221+
fields:
1222+
- name: name
1223+
type: token
1224+
1225+
- name: default_value
1226+
type: node
1227+
kind: RubyLiteralNode
1228+
1229+
- name: required
1230+
type: boolean
1231+
1232+
- name: double_splat
1233+
type: boolean
1234+
1235+
- name: ERBStrictLocalsNode
1236+
fields:
1237+
- name: tag_opening
1238+
type: token
1239+
1240+
- name: content
1241+
type: token
1242+
1243+
- name: tag_closing
1244+
type: token
1245+
1246+
- name: analyzed_ruby
1247+
type: analyzed_ruby
1248+
1249+
- name: prism_node
1250+
type: prism_node
1251+
1252+
- name: locals
1253+
type: array
1254+
kind:
1255+
- RubyStrictLocalNode
1256+
11721257
- name: ERBYieldNode
11731258
fields:
11741259
- name: tag_opening

ext/herb/extension.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ static VALUE Herb_parse(int argc, VALUE* argv, VALUE self) {
140140
if (NIL_P(render_nodes)) { render_nodes = rb_hash_lookup(options, ID2SYM(rb_intern("render_nodes"))); }
141141
if (!NIL_P(render_nodes) && RTEST(render_nodes)) { parser_options.render_nodes = true; }
142142

143+
VALUE strict_locals = rb_hash_lookup(options, rb_utf8_str_new_cstr("strict_locals"));
144+
if (NIL_P(strict_locals)) { strict_locals = rb_hash_lookup(options, ID2SYM(rb_intern("strict_locals"))); }
145+
if (!NIL_P(strict_locals) && RTEST(strict_locals)) { parser_options.strict_locals = true; }
146+
143147
VALUE prism_nodes = rb_hash_lookup(options, rb_utf8_str_new_cstr("prism_nodes"));
144148
if (NIL_P(prism_nodes)) { prism_nodes = rb_hash_lookup(options, ID2SYM(rb_intern("prism_nodes"))); }
145149
if (!NIL_P(prism_nodes) && RTEST(prism_nodes)) { parser_options.prism_nodes = true; }

ext/herb/extension_helpers.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ VALUE create_parse_result(AST_DOCUMENT_NODE_T* root, VALUE source, const parser_
9090
rb_hash_aset(kwargs, ID2SYM(rb_intern("analyze")), options->analyze ? Qtrue : Qfalse);
9191
rb_hash_aset(kwargs, ID2SYM(rb_intern("action_view_helpers")), options->action_view_helpers ? Qtrue : Qfalse);
9292
rb_hash_aset(kwargs, ID2SYM(rb_intern("render_nodes")), options->render_nodes ? Qtrue : Qfalse);
93+
rb_hash_aset(kwargs, ID2SYM(rb_intern("strict_locals")), options->strict_locals ? Qtrue : Qfalse);
9394
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_nodes")), options->prism_nodes ? Qtrue : Qfalse);
9495
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_nodes_deep")), options->prism_nodes_deep ? Qtrue : Qfalse);
9596
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_program")), options->prism_program ? Qtrue : Qfalse);

java/herb_jni.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ Java_org_herb_Herb_parse(JNIEnv* env, jclass clazz, jstring source, jobject opti
7777
parser_options.render_nodes = (renderNodes == JNI_TRUE);
7878
}
7979

80+
jmethodID getStrictLocals =
81+
(*env)->GetMethodID(env, optionsClass, "isStrictLocals", "()Z");
82+
83+
if (getStrictLocals != NULL) {
84+
jboolean strictLocals = (*env)->CallBooleanMethod(env, options, getStrictLocals);
85+
parser_options.strict_locals = (strictLocals == JNI_TRUE);
86+
}
87+
8088
jmethodID getPrismNodes =
8189
(*env)->GetMethodID(env, optionsClass, "isPrismNodes", "()Z");
8290

java/org/herb/ParserOptions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public class ParserOptions {
66
private boolean strict = true;
77
private boolean actionViewHelpers = false;
88
private boolean renderNodes = false;
9+
private boolean strictLocals = false;
910
private boolean prismNodes = false;
1011
private boolean prismNodesDeep = false;
1112
private boolean prismProgram = false;
@@ -57,6 +58,15 @@ public boolean isRenderNodes() {
5758
return renderNodes;
5859
}
5960

61+
public ParserOptions strictLocals(boolean value) {
62+
this.strictLocals = value;
63+
return this;
64+
}
65+
66+
public boolean isStrictLocals() {
67+
return strictLocals;
68+
}
69+
6070
public ParserOptions prismNodes(boolean value) {
6171
this.prismNodes = value;
6272
return this;

javascript/packages/core/src/parser-options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface ParseOptions {
44
strict?: boolean
55
action_view_helpers?: boolean
66
render_nodes?: boolean
7+
strict_locals?: boolean
78
prism_nodes?: boolean
89
prism_nodes_deep?: boolean
910
prism_program?: boolean
@@ -17,6 +18,7 @@ export const DEFAULT_PARSER_OPTIONS: SerializedParserOptions = {
1718
strict: true,
1819
action_view_helpers: false,
1920
render_nodes: false,
21+
strict_locals: false,
2022
prism_nodes: false,
2123
prism_nodes_deep: false,
2224
prism_program: false,
@@ -41,6 +43,9 @@ export class ParserOptions {
4143
/** Whether ActionView render call detection was enabled during parsing. */
4244
readonly render_nodes: boolean
4345

46+
/** Whether strict locals analysis was enabled during parsing. */
47+
readonly strict_locals: boolean
48+
4449
/** Whether Prism node serialization was enabled during parsing. */
4550
readonly prism_nodes: boolean
4651

@@ -60,6 +65,7 @@ export class ParserOptions {
6065
this.analyze = options.analyze ?? DEFAULT_PARSER_OPTIONS.analyze
6166
this.action_view_helpers = options.action_view_helpers ?? DEFAULT_PARSER_OPTIONS.action_view_helpers
6267
this.render_nodes = options.render_nodes ?? DEFAULT_PARSER_OPTIONS.render_nodes
68+
this.strict_locals = options.strict_locals ?? DEFAULT_PARSER_OPTIONS.strict_locals
6369
this.prism_nodes = options.prism_nodes ?? DEFAULT_PARSER_OPTIONS.prism_nodes
6470
this.prism_nodes_deep = options.prism_nodes_deep ?? DEFAULT_PARSER_OPTIONS.prism_nodes_deep
6571
this.prism_program = options.prism_program ?? DEFAULT_PARSER_OPTIONS.prism_program

javascript/packages/linter/test/parse-cache.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe("ParseCache", () => {
7373
prism_program: false,
7474
render_nodes: false,
7575
strict: true,
76+
strict_locals: false,
7677
action_view_helpers: false,
7778
})
7879
})
@@ -89,6 +90,7 @@ describe("ParseCache", () => {
8990
prism_program: false,
9091
render_nodes: false,
9192
strict: false,
93+
strict_locals: false,
9294
action_view_helpers: false,
9395
})
9496
})
@@ -105,6 +107,7 @@ describe("ParseCache", () => {
105107
prism_program: false,
106108
render_nodes: false,
107109
strict: true,
110+
strict_locals: false,
108111
action_view_helpers: false,
109112
})
110113
})
@@ -121,6 +124,7 @@ describe("ParseCache", () => {
121124
prism_program: false,
122125
render_nodes: false,
123126
strict: false,
127+
strict_locals: false,
124128
action_view_helpers: false,
125129
})
126130
})

javascript/packages/node/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"./extension/libherb/analyze/parse_errors.c",
2323
"./extension/libherb/analyze/prism_annotate.c",
2424
"./extension/libherb/analyze/render_nodes.c",
25+
"./extension/libherb/analyze/strict_locals.c",
2526
"./extension/libherb/analyze/transform.c",
2627
"./extension/libherb/analyze/action_view/attribute_extraction_helpers.c",
2728
"./extension/libherb/analyze/action_view/content_tag.c",

javascript/packages/node/extension/herb.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ napi_value Herb_parse(napi_env env, napi_callback_info info) {
130130
parser_options.render_nodes = render_nodes_value;
131131
}
132132

133+
napi_value strict_locals_prop;
134+
bool has_strict_locals_prop;
135+
napi_has_named_property(env, args[1], "strict_locals", &has_strict_locals_prop);
136+
137+
if (has_strict_locals_prop) {
138+
napi_get_named_property(env, args[1], "strict_locals", &strict_locals_prop);
139+
bool strict_locals_value;
140+
napi_get_value_bool(env, strict_locals_prop, &strict_locals_value);
141+
parser_options.strict_locals = strict_locals_value;
142+
}
143+
133144
napi_value prism_nodes_prop;
134145
bool has_prism_nodes_prop;
135146
napi_has_named_property(env, args[1], "prism_nodes", &has_prism_nodes_prop);

javascript/packages/printer/src/identity-printer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,14 @@ export class IdentityPrinter extends Printer {
394394
// extracted metadata, nothing to print
395395
}
396396

397+
visitERBStrictLocalsNode(node: Nodes.ERBStrictLocalsNode): void {
398+
this.printERBNode(node)
399+
}
400+
401+
visitRubyStrictLocalNode(_node: Nodes.RubyStrictLocalNode): void {
402+
// extracted metadata, nothing to print
403+
}
404+
397405
visitERBYieldNode(node: Nodes.ERBYieldNode): void {
398406
this.printERBNode(node)
399407
}

0 commit comments

Comments
 (0)