@herb-tools/linter 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/dist/herb-lint.js +1525 -98
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +546 -87
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +465 -87
  7. package/dist/index.js.map +1 -1
  8. package/dist/lint-worker.js +1523 -96
  9. package/dist/lint-worker.js.map +1 -1
  10. package/dist/loader.cjs +1078 -94
  11. package/dist/loader.cjs.map +1 -1
  12. package/dist/loader.js +1057 -95
  13. package/dist/loader.js.map +1 -1
  14. package/dist/rules/actionview-no-silent-render.js +31 -0
  15. package/dist/rules/actionview-no-silent-render.js.map +1 -0
  16. package/dist/rules/erb-no-case-node-children.js +3 -1
  17. package/dist/rules/erb-no-case-node-children.js.map +1 -1
  18. package/dist/rules/erb-no-duplicate-branch-elements.js +95 -11
  19. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -1
  20. package/dist/rules/erb-no-empty-control-flow.js +190 -0
  21. package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
  22. package/dist/rules/erb-no-silent-statement.js +44 -0
  23. package/dist/rules/erb-no-silent-statement.js.map +1 -0
  24. package/dist/rules/erb-no-unsafe-script-interpolation.js +37 -3
  25. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -1
  26. package/dist/rules/html-allowed-script-type.js +1 -1
  27. package/dist/rules/html-allowed-script-type.js.map +1 -1
  28. package/dist/rules/index.js +20 -16
  29. package/dist/rules/index.js.map +1 -1
  30. package/dist/rules/rule-utils.js +14 -23
  31. package/dist/rules/rule-utils.js.map +1 -1
  32. package/dist/rules.js +8 -2
  33. package/dist/rules.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
  36. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +1 -0
  37. package/dist/types/rules/erb-no-empty-control-flow.d.ts +8 -0
  38. package/dist/types/rules/erb-no-silent-statement.d.ts +9 -0
  39. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +2 -1
  40. package/dist/types/rules/index.d.ts +20 -16
  41. package/dist/types/rules/rule-utils.d.ts +8 -11
  42. package/dist/types/types.d.ts +4 -3
  43. package/dist/types.js +6 -3
  44. package/dist/types.js.map +1 -1
  45. package/docs/rules/README.md +3 -0
  46. package/docs/rules/actionview-no-silent-render.md +47 -0
  47. package/docs/rules/erb-no-empty-control-flow.md +83 -0
  48. package/docs/rules/erb-no-silent-statement.md +53 -0
  49. package/docs/rules/erb-no-unsafe-script-interpolation.md +70 -3
  50. package/package.json +8 -8
  51. package/src/index.ts +21 -0
  52. package/src/rules/actionview-no-silent-render.ts +44 -0
  53. package/src/rules/erb-no-case-node-children.ts +3 -1
  54. package/src/rules/erb-no-duplicate-branch-elements.ts +130 -14
  55. package/src/rules/erb-no-empty-control-flow.ts +255 -0
  56. package/src/rules/erb-no-silent-statement.ts +58 -0
  57. package/src/rules/erb-no-unsafe-script-interpolation.ts +51 -5
  58. package/src/rules/html-allowed-script-type.ts +1 -1
  59. package/src/rules/index.ts +21 -16
  60. package/src/rules/rule-utils.ts +15 -24
  61. package/src/rules.ts +8 -2
  62. package/src/types.ts +7 -3
package/dist/loader.js CHANGED
@@ -2306,7 +2306,7 @@ class Token {
2306
2306
  }
2307
2307
 
2308
2308
  // NOTE: This file is generated by the templates/template.rb script and should not
2309
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/errors.ts.erb
2309
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/errors.ts.erb
2310
2310
  class HerbError {
2311
2311
  type;
2312
2312
  message;
@@ -3307,6 +3307,279 @@ class NestedERBTagError extends HerbError {
3307
3307
  return output;
3308
3308
  }
3309
3309
  }
3310
+ class RenderAmbiguousLocalsError extends HerbError {
3311
+ partial;
3312
+ static from(data) {
3313
+ return new RenderAmbiguousLocalsError({
3314
+ type: data.type,
3315
+ message: data.message,
3316
+ location: Location.from(data.location),
3317
+ partial: data.partial,
3318
+ });
3319
+ }
3320
+ constructor(props) {
3321
+ super(props.type, props.message, props.location);
3322
+ this.partial = props.partial;
3323
+ }
3324
+ toJSON() {
3325
+ return {
3326
+ ...super.toJSON(),
3327
+ type: "RENDER_AMBIGUOUS_LOCALS_ERROR",
3328
+ partial: this.partial,
3329
+ };
3330
+ }
3331
+ toMonacoDiagnostic() {
3332
+ return {
3333
+ line: this.location.start.line,
3334
+ column: this.location.start.column,
3335
+ endLine: this.location.end.line,
3336
+ endColumn: this.location.end.column,
3337
+ message: this.message,
3338
+ severity: 'error'
3339
+ };
3340
+ }
3341
+ treeInspect() {
3342
+ let output = "";
3343
+ output += `@ RenderAmbiguousLocalsError ${this.location.treeInspectWithLabel()}\n`;
3344
+ output += `├── message: "${this.message}"\n`;
3345
+ output += `└── partial: ${JSON.stringify(this.partial)}\n`;
3346
+ return output;
3347
+ }
3348
+ }
3349
+ class RenderMissingLocalsError extends HerbError {
3350
+ partial;
3351
+ keywords;
3352
+ static from(data) {
3353
+ return new RenderMissingLocalsError({
3354
+ type: data.type,
3355
+ message: data.message,
3356
+ location: Location.from(data.location),
3357
+ partial: data.partial,
3358
+ keywords: data.keywords,
3359
+ });
3360
+ }
3361
+ constructor(props) {
3362
+ super(props.type, props.message, props.location);
3363
+ this.partial = props.partial;
3364
+ this.keywords = props.keywords;
3365
+ }
3366
+ toJSON() {
3367
+ return {
3368
+ ...super.toJSON(),
3369
+ type: "RENDER_MISSING_LOCALS_ERROR",
3370
+ partial: this.partial,
3371
+ keywords: this.keywords,
3372
+ };
3373
+ }
3374
+ toMonacoDiagnostic() {
3375
+ return {
3376
+ line: this.location.start.line,
3377
+ column: this.location.start.column,
3378
+ endLine: this.location.end.line,
3379
+ endColumn: this.location.end.column,
3380
+ message: this.message,
3381
+ severity: 'error'
3382
+ };
3383
+ }
3384
+ treeInspect() {
3385
+ let output = "";
3386
+ output += `@ RenderMissingLocalsError ${this.location.treeInspectWithLabel()}\n`;
3387
+ output += `├── message: "${this.message}"\n`;
3388
+ output += `├── partial: ${JSON.stringify(this.partial)}\n`;
3389
+ output += `└── keywords: ${JSON.stringify(this.keywords)}\n`;
3390
+ return output;
3391
+ }
3392
+ }
3393
+ class RenderNoArgumentsError extends HerbError {
3394
+ static from(data) {
3395
+ return new RenderNoArgumentsError({
3396
+ type: data.type,
3397
+ message: data.message,
3398
+ location: Location.from(data.location),
3399
+ });
3400
+ }
3401
+ constructor(props) {
3402
+ super(props.type, props.message, props.location);
3403
+ }
3404
+ toJSON() {
3405
+ return {
3406
+ ...super.toJSON(),
3407
+ type: "RENDER_NO_ARGUMENTS_ERROR",
3408
+ };
3409
+ }
3410
+ toMonacoDiagnostic() {
3411
+ return {
3412
+ line: this.location.start.line,
3413
+ column: this.location.start.column,
3414
+ endLine: this.location.end.line,
3415
+ endColumn: this.location.end.column,
3416
+ message: this.message,
3417
+ severity: 'error'
3418
+ };
3419
+ }
3420
+ treeInspect() {
3421
+ let output = "";
3422
+ output += `@ RenderNoArgumentsError ${this.location.treeInspectWithLabel()}\n`;
3423
+ output += `└── message: "${this.message}"\n`;
3424
+ return output;
3425
+ }
3426
+ }
3427
+ class RenderConflictingPartialError extends HerbError {
3428
+ positional_partial;
3429
+ keyword_partial;
3430
+ static from(data) {
3431
+ return new RenderConflictingPartialError({
3432
+ type: data.type,
3433
+ message: data.message,
3434
+ location: Location.from(data.location),
3435
+ positional_partial: data.positional_partial,
3436
+ keyword_partial: data.keyword_partial,
3437
+ });
3438
+ }
3439
+ constructor(props) {
3440
+ super(props.type, props.message, props.location);
3441
+ this.positional_partial = props.positional_partial;
3442
+ this.keyword_partial = props.keyword_partial;
3443
+ }
3444
+ toJSON() {
3445
+ return {
3446
+ ...super.toJSON(),
3447
+ type: "RENDER_CONFLICTING_PARTIAL_ERROR",
3448
+ positional_partial: this.positional_partial,
3449
+ keyword_partial: this.keyword_partial,
3450
+ };
3451
+ }
3452
+ toMonacoDiagnostic() {
3453
+ return {
3454
+ line: this.location.start.line,
3455
+ column: this.location.start.column,
3456
+ endLine: this.location.end.line,
3457
+ endColumn: this.location.end.column,
3458
+ message: this.message,
3459
+ severity: 'error'
3460
+ };
3461
+ }
3462
+ treeInspect() {
3463
+ let output = "";
3464
+ output += `@ RenderConflictingPartialError ${this.location.treeInspectWithLabel()}\n`;
3465
+ output += `├── message: "${this.message}"\n`;
3466
+ output += `├── positional_partial: ${JSON.stringify(this.positional_partial)}\n`;
3467
+ output += `└── keyword_partial: ${JSON.stringify(this.keyword_partial)}\n`;
3468
+ return output;
3469
+ }
3470
+ }
3471
+ class RenderInvalidAsOptionError extends HerbError {
3472
+ as_value;
3473
+ static from(data) {
3474
+ return new RenderInvalidAsOptionError({
3475
+ type: data.type,
3476
+ message: data.message,
3477
+ location: Location.from(data.location),
3478
+ as_value: data.as_value,
3479
+ });
3480
+ }
3481
+ constructor(props) {
3482
+ super(props.type, props.message, props.location);
3483
+ this.as_value = props.as_value;
3484
+ }
3485
+ toJSON() {
3486
+ return {
3487
+ ...super.toJSON(),
3488
+ type: "RENDER_INVALID_AS_OPTION_ERROR",
3489
+ as_value: this.as_value,
3490
+ };
3491
+ }
3492
+ toMonacoDiagnostic() {
3493
+ return {
3494
+ line: this.location.start.line,
3495
+ column: this.location.start.column,
3496
+ endLine: this.location.end.line,
3497
+ endColumn: this.location.end.column,
3498
+ message: this.message,
3499
+ severity: 'error'
3500
+ };
3501
+ }
3502
+ treeInspect() {
3503
+ let output = "";
3504
+ output += `@ RenderInvalidAsOptionError ${this.location.treeInspectWithLabel()}\n`;
3505
+ output += `├── message: "${this.message}"\n`;
3506
+ output += `└── as_value: ${JSON.stringify(this.as_value)}\n`;
3507
+ return output;
3508
+ }
3509
+ }
3510
+ class RenderObjectAndCollectionError extends HerbError {
3511
+ static from(data) {
3512
+ return new RenderObjectAndCollectionError({
3513
+ type: data.type,
3514
+ message: data.message,
3515
+ location: Location.from(data.location),
3516
+ });
3517
+ }
3518
+ constructor(props) {
3519
+ super(props.type, props.message, props.location);
3520
+ }
3521
+ toJSON() {
3522
+ return {
3523
+ ...super.toJSON(),
3524
+ type: "RENDER_OBJECT_AND_COLLECTION_ERROR",
3525
+ };
3526
+ }
3527
+ toMonacoDiagnostic() {
3528
+ return {
3529
+ line: this.location.start.line,
3530
+ column: this.location.start.column,
3531
+ endLine: this.location.end.line,
3532
+ endColumn: this.location.end.column,
3533
+ message: this.message,
3534
+ severity: 'error'
3535
+ };
3536
+ }
3537
+ treeInspect() {
3538
+ let output = "";
3539
+ output += `@ RenderObjectAndCollectionError ${this.location.treeInspectWithLabel()}\n`;
3540
+ output += `└── message: "${this.message}"\n`;
3541
+ return output;
3542
+ }
3543
+ }
3544
+ class RenderLayoutWithoutBlockError extends HerbError {
3545
+ layout;
3546
+ static from(data) {
3547
+ return new RenderLayoutWithoutBlockError({
3548
+ type: data.type,
3549
+ message: data.message,
3550
+ location: Location.from(data.location),
3551
+ layout: data.layout,
3552
+ });
3553
+ }
3554
+ constructor(props) {
3555
+ super(props.type, props.message, props.location);
3556
+ this.layout = props.layout;
3557
+ }
3558
+ toJSON() {
3559
+ return {
3560
+ ...super.toJSON(),
3561
+ type: "RENDER_LAYOUT_WITHOUT_BLOCK_ERROR",
3562
+ layout: this.layout,
3563
+ };
3564
+ }
3565
+ toMonacoDiagnostic() {
3566
+ return {
3567
+ line: this.location.start.line,
3568
+ column: this.location.start.column,
3569
+ endLine: this.location.end.line,
3570
+ endColumn: this.location.end.column,
3571
+ message: this.message,
3572
+ severity: 'error'
3573
+ };
3574
+ }
3575
+ treeInspect() {
3576
+ let output = "";
3577
+ output += `@ RenderLayoutWithoutBlockError ${this.location.treeInspectWithLabel()}\n`;
3578
+ output += `├── message: "${this.message}"\n`;
3579
+ output += `└── layout: ${JSON.stringify(this.layout)}\n`;
3580
+ return output;
3581
+ }
3582
+ }
3310
3583
  function fromSerializedError(error) {
3311
3584
  switch (error.type) {
3312
3585
  case "UNEXPECTED_ERROR": return UnexpectedError.from(error);
@@ -3332,6 +3605,13 @@ function fromSerializedError(error) {
3332
3605
  case "UNCLOSED_ERB_TAG_ERROR": return UnclosedERBTagError.from(error);
3333
3606
  case "STRAY_ERB_CLOSING_TAG_ERROR": return StrayERBClosingTagError.from(error);
3334
3607
  case "NESTED_ERB_TAG_ERROR": return NestedERBTagError.from(error);
3608
+ case "RENDER_AMBIGUOUS_LOCALS_ERROR": return RenderAmbiguousLocalsError.from(error);
3609
+ case "RENDER_MISSING_LOCALS_ERROR": return RenderMissingLocalsError.from(error);
3610
+ case "RENDER_NO_ARGUMENTS_ERROR": return RenderNoArgumentsError.from(error);
3611
+ case "RENDER_CONFLICTING_PARTIAL_ERROR": return RenderConflictingPartialError.from(error);
3612
+ case "RENDER_INVALID_AS_OPTION_ERROR": return RenderInvalidAsOptionError.from(error);
3613
+ case "RENDER_OBJECT_AND_COLLECTION_ERROR": return RenderObjectAndCollectionError.from(error);
3614
+ case "RENDER_LAYOUT_WITHOUT_BLOCK_ERROR": return RenderLayoutWithoutBlockError.from(error);
3335
3615
  default:
3336
3616
  throw new Error(`Unknown node type: ${error.type}`);
3337
3617
  }
@@ -23347,7 +23627,7 @@ function deserializePrismNode(bytes, source) {
23347
23627
  }
23348
23628
 
23349
23629
  // NOTE: This file is generated by the templates/template.rb script and should not
23350
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/nodes.ts.erb
23630
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/nodes.ts.erb
23351
23631
  class Node {
23352
23632
  type;
23353
23633
  location;
@@ -25891,6 +26171,225 @@ class ERBUnlessNode extends Node {
25891
26171
  return output;
25892
26172
  }
25893
26173
  }
26174
+ class RubyRenderLocalNode extends Node {
26175
+ name;
26176
+ value;
26177
+ static get type() {
26178
+ return "AST_RUBY_RENDER_LOCAL_NODE";
26179
+ }
26180
+ static from(data) {
26181
+ return new RubyRenderLocalNode({
26182
+ type: data.type,
26183
+ location: Location.from(data.location),
26184
+ errors: (data.errors || []).map(error => HerbError.from(error)),
26185
+ name: data.name ? Token.from(data.name) : null,
26186
+ value: data.value ? fromSerializedNode((data.value)) : null,
26187
+ });
26188
+ }
26189
+ constructor(props) {
26190
+ super(props.type, props.location, props.errors);
26191
+ this.name = props.name;
26192
+ this.value = props.value;
26193
+ }
26194
+ accept(visitor) {
26195
+ visitor.visitRubyRenderLocalNode(this);
26196
+ }
26197
+ childNodes() {
26198
+ return [
26199
+ this.value,
26200
+ ];
26201
+ }
26202
+ compactChildNodes() {
26203
+ return this.childNodes().filter(node => node !== null && node !== undefined);
26204
+ }
26205
+ recursiveErrors() {
26206
+ return [
26207
+ ...this.errors,
26208
+ this.value ? this.value.recursiveErrors() : [],
26209
+ ].flat();
26210
+ }
26211
+ toJSON() {
26212
+ return {
26213
+ ...super.toJSON(),
26214
+ type: "AST_RUBY_RENDER_LOCAL_NODE",
26215
+ name: this.name ? this.name.toJSON() : null,
26216
+ value: this.value ? this.value.toJSON() : null,
26217
+ };
26218
+ }
26219
+ treeInspect() {
26220
+ let output = "";
26221
+ output += `@ RubyRenderLocalNode ${this.location.treeInspectWithLabel()}\n`;
26222
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
26223
+ output += `├── name: ${this.name ? this.name.treeInspect() : "∅"}\n`;
26224
+ output += `└── value: ${this.inspectNode(this.value, " ")}`;
26225
+ return output;
26226
+ }
26227
+ }
26228
+ class ERBRenderNode extends Node {
26229
+ tag_opening;
26230
+ content;
26231
+ tag_closing;
26232
+ // no-op for analyzed_ruby
26233
+ prism_node;
26234
+ partial;
26235
+ template_path;
26236
+ layout;
26237
+ file;
26238
+ inline_template;
26239
+ body;
26240
+ plain;
26241
+ html;
26242
+ renderable;
26243
+ collection;
26244
+ object;
26245
+ as_name;
26246
+ spacer_template;
26247
+ formats;
26248
+ variants;
26249
+ handlers;
26250
+ content_type;
26251
+ locals;
26252
+ static get type() {
26253
+ return "AST_ERB_RENDER_NODE";
26254
+ }
26255
+ static from(data) {
26256
+ return new ERBRenderNode({
26257
+ type: data.type,
26258
+ location: Location.from(data.location),
26259
+ errors: (data.errors || []).map(error => HerbError.from(error)),
26260
+ tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
26261
+ content: data.content ? Token.from(data.content) : null,
26262
+ tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
26263
+ // no-op for analyzed_ruby
26264
+ prism_node: data.prism_node ? new Uint8Array(data.prism_node) : null,
26265
+ partial: data.partial ? Token.from(data.partial) : null,
26266
+ template_path: data.template_path ? Token.from(data.template_path) : null,
26267
+ layout: data.layout ? Token.from(data.layout) : null,
26268
+ file: data.file ? Token.from(data.file) : null,
26269
+ inline_template: data.inline_template ? Token.from(data.inline_template) : null,
26270
+ body: data.body ? Token.from(data.body) : null,
26271
+ plain: data.plain ? Token.from(data.plain) : null,
26272
+ html: data.html ? Token.from(data.html) : null,
26273
+ renderable: data.renderable ? Token.from(data.renderable) : null,
26274
+ collection: data.collection ? Token.from(data.collection) : null,
26275
+ object: data.object ? Token.from(data.object) : null,
26276
+ as_name: data.as_name ? Token.from(data.as_name) : null,
26277
+ spacer_template: data.spacer_template ? Token.from(data.spacer_template) : null,
26278
+ formats: data.formats ? Token.from(data.formats) : null,
26279
+ variants: data.variants ? Token.from(data.variants) : null,
26280
+ handlers: data.handlers ? Token.from(data.handlers) : null,
26281
+ content_type: data.content_type ? Token.from(data.content_type) : null,
26282
+ locals: (data.locals || []).map(node => fromSerializedNode(node)),
26283
+ });
26284
+ }
26285
+ constructor(props) {
26286
+ super(props.type, props.location, props.errors);
26287
+ this.tag_opening = props.tag_opening;
26288
+ this.content = props.content;
26289
+ this.tag_closing = props.tag_closing;
26290
+ // no-op for analyzed_ruby
26291
+ this.prism_node = props.prism_node;
26292
+ this.partial = props.partial;
26293
+ this.template_path = props.template_path;
26294
+ this.layout = props.layout;
26295
+ this.file = props.file;
26296
+ this.inline_template = props.inline_template;
26297
+ this.body = props.body;
26298
+ this.plain = props.plain;
26299
+ this.html = props.html;
26300
+ this.renderable = props.renderable;
26301
+ this.collection = props.collection;
26302
+ this.object = props.object;
26303
+ this.as_name = props.as_name;
26304
+ this.spacer_template = props.spacer_template;
26305
+ this.formats = props.formats;
26306
+ this.variants = props.variants;
26307
+ this.handlers = props.handlers;
26308
+ this.content_type = props.content_type;
26309
+ this.locals = props.locals;
26310
+ }
26311
+ accept(visitor) {
26312
+ visitor.visitERBRenderNode(this);
26313
+ }
26314
+ childNodes() {
26315
+ return [
26316
+ ...this.locals,
26317
+ ];
26318
+ }
26319
+ compactChildNodes() {
26320
+ return this.childNodes().filter(node => node !== null && node !== undefined);
26321
+ }
26322
+ get prismNode() {
26323
+ if (!this.prism_node || !this.source)
26324
+ return null;
26325
+ return deserializePrismNode(this.prism_node, this.source);
26326
+ }
26327
+ recursiveErrors() {
26328
+ return [
26329
+ ...this.errors,
26330
+ ...this.locals.map(node => node.recursiveErrors()),
26331
+ ].flat();
26332
+ }
26333
+ toJSON() {
26334
+ return {
26335
+ ...super.toJSON(),
26336
+ type: "AST_ERB_RENDER_NODE",
26337
+ tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
26338
+ content: this.content ? this.content.toJSON() : null,
26339
+ tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
26340
+ // no-op for analyzed_ruby
26341
+ prism_node: this.prism_node ? Array.from(this.prism_node) : null,
26342
+ partial: this.partial ? this.partial.toJSON() : null,
26343
+ template_path: this.template_path ? this.template_path.toJSON() : null,
26344
+ layout: this.layout ? this.layout.toJSON() : null,
26345
+ file: this.file ? this.file.toJSON() : null,
26346
+ inline_template: this.inline_template ? this.inline_template.toJSON() : null,
26347
+ body: this.body ? this.body.toJSON() : null,
26348
+ plain: this.plain ? this.plain.toJSON() : null,
26349
+ html: this.html ? this.html.toJSON() : null,
26350
+ renderable: this.renderable ? this.renderable.toJSON() : null,
26351
+ collection: this.collection ? this.collection.toJSON() : null,
26352
+ object: this.object ? this.object.toJSON() : null,
26353
+ as_name: this.as_name ? this.as_name.toJSON() : null,
26354
+ spacer_template: this.spacer_template ? this.spacer_template.toJSON() : null,
26355
+ formats: this.formats ? this.formats.toJSON() : null,
26356
+ variants: this.variants ? this.variants.toJSON() : null,
26357
+ handlers: this.handlers ? this.handlers.toJSON() : null,
26358
+ content_type: this.content_type ? this.content_type.toJSON() : null,
26359
+ locals: this.locals.map(node => node.toJSON()),
26360
+ };
26361
+ }
26362
+ treeInspect() {
26363
+ let output = "";
26364
+ output += `@ ERBRenderNode ${this.location.treeInspectWithLabel()}\n`;
26365
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
26366
+ output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
26367
+ output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
26368
+ output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
26369
+ if (this.prism_node) {
26370
+ output += `├── prism_node: ${this.source ? inspectPrismSerialized(this.prism_node, this.source, "│ ") : `(${this.prism_node.length} bytes)`}\n`;
26371
+ }
26372
+ output += `├── partial: ${this.partial ? this.partial.treeInspect() : "∅"}\n`;
26373
+ output += `├── template_path: ${this.template_path ? this.template_path.treeInspect() : "∅"}\n`;
26374
+ output += `├── layout: ${this.layout ? this.layout.treeInspect() : "∅"}\n`;
26375
+ output += `├── file: ${this.file ? this.file.treeInspect() : "∅"}\n`;
26376
+ output += `├── inline_template: ${this.inline_template ? this.inline_template.treeInspect() : "∅"}\n`;
26377
+ output += `├── body: ${this.body ? this.body.treeInspect() : "∅"}\n`;
26378
+ output += `├── plain: ${this.plain ? this.plain.treeInspect() : "∅"}\n`;
26379
+ output += `├── html: ${this.html ? this.html.treeInspect() : "∅"}\n`;
26380
+ output += `├── renderable: ${this.renderable ? this.renderable.treeInspect() : "∅"}\n`;
26381
+ output += `├── collection: ${this.collection ? this.collection.treeInspect() : "∅"}\n`;
26382
+ output += `├── object: ${this.object ? this.object.treeInspect() : "∅"}\n`;
26383
+ output += `├── as_name: ${this.as_name ? this.as_name.treeInspect() : "∅"}\n`;
26384
+ output += `├── spacer_template: ${this.spacer_template ? this.spacer_template.treeInspect() : "∅"}\n`;
26385
+ output += `├── formats: ${this.formats ? this.formats.treeInspect() : "∅"}\n`;
26386
+ output += `├── variants: ${this.variants ? this.variants.treeInspect() : "∅"}\n`;
26387
+ output += `├── handlers: ${this.handlers ? this.handlers.treeInspect() : "∅"}\n`;
26388
+ output += `├── content_type: ${this.content_type ? this.content_type.treeInspect() : "∅"}\n`;
26389
+ output += `└── locals: ${this.inspectArray(this.locals, " ")}`;
26390
+ return output;
26391
+ }
26392
+ }
25894
26393
  class ERBYieldNode extends Node {
25895
26394
  tag_opening;
25896
26395
  content;
@@ -26054,6 +26553,8 @@ function fromSerializedNode(node) {
26054
26553
  case "AST_ERB_ENSURE_NODE": return ERBEnsureNode.from(node);
26055
26554
  case "AST_ERB_BEGIN_NODE": return ERBBeginNode.from(node);
26056
26555
  case "AST_ERB_UNLESS_NODE": return ERBUnlessNode.from(node);
26556
+ case "AST_RUBY_RENDER_LOCAL_NODE": return RubyRenderLocalNode.from(node);
26557
+ case "AST_ERB_RENDER_NODE": return ERBRenderNode.from(node);
26057
26558
  case "AST_ERB_YIELD_NODE": return ERBYieldNode.from(node);
26058
26559
  case "AST_ERB_IN_NODE": return ERBInNode.from(node);
26059
26560
  default:
@@ -26103,6 +26604,7 @@ const DEFAULT_PARSER_OPTIONS = {
26103
26604
  analyze: true,
26104
26605
  strict: true,
26105
26606
  action_view_helpers: false,
26607
+ render_nodes: false,
26106
26608
  prism_nodes: false,
26107
26609
  prism_nodes_deep: false,
26108
26610
  prism_program: false,
@@ -26119,6 +26621,8 @@ class ParserOptions {
26119
26621
  analyze;
26120
26622
  /** Whether ActionView tag helper transformation was enabled during parsing. */
26121
26623
  action_view_helpers;
26624
+ /** Whether ActionView render call detection was enabled during parsing. */
26625
+ render_nodes;
26122
26626
  /** Whether Prism node serialization was enabled during parsing. */
26123
26627
  prism_nodes;
26124
26628
  /** Whether deep Prism node serialization was enabled during parsing. */
@@ -26133,6 +26637,7 @@ class ParserOptions {
26133
26637
  this.track_whitespace = options.track_whitespace ?? DEFAULT_PARSER_OPTIONS.track_whitespace;
26134
26638
  this.analyze = options.analyze ?? DEFAULT_PARSER_OPTIONS.analyze;
26135
26639
  this.action_view_helpers = options.action_view_helpers ?? DEFAULT_PARSER_OPTIONS.action_view_helpers;
26640
+ this.render_nodes = options.render_nodes ?? DEFAULT_PARSER_OPTIONS.render_nodes;
26136
26641
  this.prism_nodes = options.prism_nodes ?? DEFAULT_PARSER_OPTIONS.prism_nodes;
26137
26642
  this.prism_nodes_deep = options.prism_nodes_deep ?? DEFAULT_PARSER_OPTIONS.prism_nodes_deep;
26138
26643
  this.prism_program = options.prism_program ?? DEFAULT_PARSER_OPTIONS.prism_program;
@@ -26212,7 +26717,7 @@ class ParseResult extends Result {
26212
26717
  }
26213
26718
 
26214
26719
  // NOTE: This file is generated by the templates/template.rb script and should not
26215
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/node-type-guards.ts.erb
26720
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/node-type-guards.ts.erb
26216
26721
  /**
26217
26722
  * Type guard functions for AST nodes.
26218
26723
  * These functions provide type checking by combining both instanceof
@@ -26507,6 +27012,22 @@ function isERBUnlessNode(node) {
26507
27012
  return false;
26508
27013
  return node instanceof ERBUnlessNode || node.type === "AST_ERB_UNLESS_NODE" || node.constructor.type === "AST_ERB_UNLESS_NODE";
26509
27014
  }
27015
+ /**
27016
+ * Checks if a node is a RubyRenderLocalNode
27017
+ */
27018
+ function isRubyRenderLocalNode(node) {
27019
+ if (!node)
27020
+ return false;
27021
+ return node instanceof RubyRenderLocalNode || node.type === "AST_RUBY_RENDER_LOCAL_NODE" || node.constructor.type === "AST_RUBY_RENDER_LOCAL_NODE";
27022
+ }
27023
+ /**
27024
+ * Checks if a node is a ERBRenderNode
27025
+ */
27026
+ function isERBRenderNode(node) {
27027
+ if (!node)
27028
+ return false;
27029
+ return node instanceof ERBRenderNode || node.type === "AST_ERB_RENDER_NODE" || node.constructor.type === "AST_ERB_RENDER_NODE";
27030
+ }
26510
27031
  /**
26511
27032
  * Checks if a node is a ERBYieldNode
26512
27033
  */
@@ -26543,6 +27064,7 @@ function isERBNode(node) {
26543
27064
  isERBEnsureNode(node) ||
26544
27065
  isERBBeginNode(node) ||
26545
27066
  isERBUnlessNode(node) ||
27067
+ isERBRenderNode(node) ||
26546
27068
  isERBYieldNode(node) ||
26547
27069
  isERBInNode(node);
26548
27070
  }
@@ -26593,6 +27115,8 @@ const NODE_TYPE_GUARDS = new Map([
26593
27115
  [ERBEnsureNode, isERBEnsureNode],
26594
27116
  [ERBBeginNode, isERBBeginNode],
26595
27117
  [ERBUnlessNode, isERBUnlessNode],
27118
+ [RubyRenderLocalNode, isRubyRenderLocalNode],
27119
+ [ERBRenderNode, isERBRenderNode],
26596
27120
  [ERBYieldNode, isERBYieldNode],
26597
27121
  [ERBInNode, isERBInNode],
26598
27122
  ]);
@@ -26643,6 +27167,8 @@ const AST_TYPE_GUARDS = new Map([
26643
27167
  ["AST_ERB_ENSURE_NODE", isERBEnsureNode],
26644
27168
  ["AST_ERB_BEGIN_NODE", isERBBeginNode],
26645
27169
  ["AST_ERB_UNLESS_NODE", isERBUnlessNode],
27170
+ ["AST_RUBY_RENDER_LOCAL_NODE", isRubyRenderLocalNode],
27171
+ ["AST_ERB_RENDER_NODE", isERBRenderNode],
26646
27172
  ["AST_ERB_YIELD_NODE", isERBYieldNode],
26647
27173
  ["AST_ERB_IN_NODE", isERBInNode],
26648
27174
  ]);
@@ -26789,6 +27315,12 @@ function getStaticContentFromNodes(nodes) {
26789
27315
  }
26790
27316
  return literalNodes.map(node => node.content).join("");
26791
27317
  }
27318
+ /**
27319
+ * Checks if nodes contain any literal content (for static validation)
27320
+ */
27321
+ function hasStaticContent(nodes) {
27322
+ return nodes.some(isLiteralNode);
27323
+ }
26792
27324
  /**
26793
27325
  * Checks if nodes are effectively static (only literals and non-output ERB)
26794
27326
  * Non-output ERB like <% if %> doesn't affect static validation
@@ -26831,7 +27363,7 @@ function getCombinedStringFromNodes(nodes) {
26831
27363
  /**
26832
27364
  * Checks if an HTML attribute name node has dynamic content (contains ERB)
26833
27365
  */
26834
- function hasDynamicAttributeName(attributeNameNode) {
27366
+ function hasDynamicAttributeNameNode(attributeNameNode) {
26835
27367
  if (!attributeNameNode.children) {
26836
27368
  return false;
26837
27369
  }
@@ -26905,7 +27437,9 @@ function getAttributeName(attributeNode, lowercase = true) {
26905
27437
  return staticName ? staticName.toLowerCase() : null;
26906
27438
  }
26907
27439
  function hasStaticAttributeValue(nodeOrAttribute, attributeName) {
26908
- const attributeNode = nodeOrAttribute;
27440
+ const attributeNode = attributeName
27441
+ ? getAttribute(nodeOrAttribute, attributeName)
27442
+ : nodeOrAttribute;
26909
27443
  if (!attributeNode?.value?.children)
26910
27444
  return false;
26911
27445
  return attributeNode.value.children.every(isLiteralNode);
@@ -26964,12 +27498,11 @@ function hasAttribute(node, attributeName) {
26964
27498
  }
26965
27499
  /**
26966
27500
  * Checks if an attribute has a dynamic (ERB-containing) name.
26967
- * Accepts an HTMLAttributeNode (wraps the core HTMLAttributeNameNode-level check).
26968
27501
  */
26969
- function hasDynamicAttributeNameOnAttribute(attributeNode) {
27502
+ function hasDynamicAttributeName(attributeNode) {
26970
27503
  if (!isHTMLAttributeNameNode(attributeNode.name))
26971
27504
  return false;
26972
- return hasDynamicAttributeName(attributeNode.name);
27505
+ return hasDynamicAttributeNameNode(attributeNode.name);
26973
27506
  }
26974
27507
  /**
26975
27508
  * Gets the combined string representation of an attribute name (including ERB syntax).
@@ -26980,12 +27513,34 @@ function getCombinedAttributeNameString(attributeNode) {
26980
27513
  return "";
26981
27514
  return getCombinedAttributeName(attributeNode.name);
26982
27515
  }
27516
+ /**
27517
+ * Checks if an attribute value contains dynamic content (ERB)
27518
+ */
27519
+ function hasDynamicAttributeValue(attributeNode) {
27520
+ if (!attributeNode.value?.children)
27521
+ return false;
27522
+ return attributeNode.value.children.some(isERBContentNode);
27523
+ }
26983
27524
  /**
26984
27525
  * Gets the value nodes array from an attribute for dynamic inspection
26985
27526
  */
26986
27527
  function getAttributeValueNodes(attributeNode) {
26987
27528
  return attributeNode.value?.children || [];
26988
27529
  }
27530
+ /**
27531
+ * Checks if an attribute value contains any static content (for validation purposes)
27532
+ */
27533
+ function hasStaticAttributeValueContent(attributeNode) {
27534
+ return hasStaticContent(getAttributeValueNodes(attributeNode));
27535
+ }
27536
+ /**
27537
+ * Gets the static content of an attribute value (all literal parts combined).
27538
+ * Unlike getStaticAttributeValue, this extracts only the static portions from mixed content.
27539
+ * Returns the concatenated literal content, or null if no literal nodes exist.
27540
+ */
27541
+ function getStaticAttributeValueContent(attributeNode) {
27542
+ return getStaticContentFromNodes(getAttributeValueNodes(attributeNode));
27543
+ }
26989
27544
  /**
26990
27545
  * Gets the combined attribute value including both static text and ERB tag syntax.
26991
27546
  * For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>").
@@ -27029,6 +27584,14 @@ function getAttributeValueQuoteType(node) {
27029
27584
  }
27030
27585
  return "none";
27031
27586
  }
27587
+ /**
27588
+ * Checks if an attribute value is quoted
27589
+ */
27590
+ function isAttributeValueQuoted(attributeNode) {
27591
+ if (!isHTMLAttributeValueNode(attributeNode.value))
27592
+ return false;
27593
+ return !!attributeNode.value.quoted;
27594
+ }
27032
27595
  /**
27033
27596
  * Iterates over all attributes of an element or open tag node
27034
27597
  */
@@ -27322,6 +27885,19 @@ function createWhitespaceNode() {
27322
27885
  });
27323
27886
  }
27324
27887
 
27888
+ // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
27889
+ const HTML_BOOLEAN_ATTRIBUTES = new Set([
27890
+ "allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact",
27891
+ "controls", "declare", "default", "defer", "disabled", "formnovalidate",
27892
+ "hidden", "inert", "ismap", "itemscope", "loop", "multiple", "muted",
27893
+ "nomodule", "nohref", "noresize", "noshade", "novalidate", "nowrap",
27894
+ "open", "playsinline", "readonly", "required", "reversed", "scoped",
27895
+ "seamless", "selected", "sortable", "truespeed", "typemustmatch",
27896
+ ]);
27897
+ function isBooleanAttribute(attributeName) {
27898
+ return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
27899
+ }
27900
+
27325
27901
  /*
27326
27902
  * The following code is derived from the "js-levenshtein" repository,
27327
27903
  * Copyright (c) 2017 Gustaf Andersson (https://github.com/gustf/js-levenshtein)
@@ -27468,7 +28044,7 @@ function didyoumean(input, list, threshold) {
27468
28044
  }
27469
28045
 
27470
28046
  // NOTE: This file is generated by the templates/template.rb script and should not
27471
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/visitor.ts.erb
28047
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/visitor.ts.erb
27472
28048
  class Visitor {
27473
28049
  visit(node) {
27474
28050
  if (!node)
@@ -27647,6 +28223,15 @@ class Visitor {
27647
28223
  this.visitERBNode(node);
27648
28224
  this.visitChildNodes(node);
27649
28225
  }
28226
+ visitRubyRenderLocalNode(node) {
28227
+ this.visitNode(node);
28228
+ this.visitChildNodes(node);
28229
+ }
28230
+ visitERBRenderNode(node) {
28231
+ this.visitNode(node);
28232
+ this.visitERBNode(node);
28233
+ this.visitChildNodes(node);
28234
+ }
27650
28235
  visitERBYieldNode(node) {
27651
28236
  this.visitNode(node);
27652
28237
  this.visitERBNode(node);
@@ -28097,6 +28682,12 @@ class IdentityPrinter extends Printer {
28097
28682
  this.visit(node.end_node);
28098
28683
  }
28099
28684
  }
28685
+ visitERBRenderNode(node) {
28686
+ this.printERBNode(node);
28687
+ }
28688
+ visitRubyRenderLocalNode(_node) {
28689
+ // extracted metadata, nothing to print
28690
+ }
28100
28691
  visitERBYieldNode(node) {
28101
28692
  this.printERBNode(node);
28102
28693
  }
@@ -28647,7 +29238,7 @@ class ParserRule {
28647
29238
  get parserOptions() {
28648
29239
  return DEFAULT_LINTER_PARSER_OPTIONS;
28649
29240
  }
28650
- createOffense(message, location, autofixContext, severity) {
29241
+ createOffense(message, location, autofixContext, severity, tags) {
28651
29242
  return {
28652
29243
  rule: this.ruleName,
28653
29244
  code: this.ruleName,
@@ -28656,6 +29247,7 @@ class ParserRule {
28656
29247
  location,
28657
29248
  autofixContext,
28658
29249
  severity,
29250
+ tags,
28659
29251
  };
28660
29252
  }
28661
29253
  }
@@ -28675,7 +29267,7 @@ class LexerRule {
28675
29267
  get defaultConfig() {
28676
29268
  return DEFAULT_RULE_CONFIG;
28677
29269
  }
28678
- createOffense(message, location, autofixContext, severity) {
29270
+ createOffense(message, location, autofixContext, severity, tags) {
28679
29271
  return {
28680
29272
  rule: this.ruleName,
28681
29273
  code: this.ruleName,
@@ -28684,6 +29276,7 @@ class LexerRule {
28684
29276
  location,
28685
29277
  autofixContext,
28686
29278
  severity,
29279
+ tags,
28687
29280
  };
28688
29281
  }
28689
29282
  }
@@ -28709,7 +29302,7 @@ class SourceRule {
28709
29302
  get defaultConfig() {
28710
29303
  return DEFAULT_RULE_CONFIG;
28711
29304
  }
28712
- createOffense(message, location, autofixContext, severity) {
29305
+ createOffense(message, location, autofixContext, severity, tags) {
28713
29306
  return {
28714
29307
  rule: this.ruleName,
28715
29308
  code: this.ruleName,
@@ -28718,6 +29311,7 @@ class SourceRule {
28718
29311
  location,
28719
29312
  autofixContext,
28720
29313
  severity,
29314
+ tags,
28721
29315
  };
28722
29316
  }
28723
29317
  }
@@ -28743,7 +29337,7 @@ class BaseRuleVisitor extends Visitor {
28743
29337
  * Helper method to create an unbound lint offense (without severity).
28744
29338
  * The Linter will bind severity based on the rule's config.
28745
29339
  */
28746
- createOffense(message, location, autofixContext, severity) {
29340
+ createOffense(message, location, autofixContext, severity, tags) {
28747
29341
  return {
28748
29342
  rule: this.ruleName,
28749
29343
  code: this.ruleName,
@@ -28752,13 +29346,14 @@ class BaseRuleVisitor extends Visitor {
28752
29346
  location,
28753
29347
  autofixContext,
28754
29348
  severity,
29349
+ tags,
28755
29350
  };
28756
29351
  }
28757
29352
  /**
28758
29353
  * Helper method to add an offense to the offenses array
28759
29354
  */
28760
- addOffense(message, location, autofixContext, severity) {
28761
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29355
+ addOffense(message, location, autofixContext, severity, tags) {
29356
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
28762
29357
  }
28763
29358
  }
28764
29359
  /**
@@ -28845,13 +29440,6 @@ const HTML_VOID_ELEMENTS = new Set([
28845
29440
  "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
28846
29441
  "param", "source", "track", "wbr",
28847
29442
  ]);
28848
- const HTML_BOOLEAN_ATTRIBUTES = new Set([
28849
- "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
28850
- "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
28851
- "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
28852
- "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
28853
- "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
28854
- ]);
28855
29443
  const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
28856
29444
  /**
28857
29445
  * SVG elements that use camelCase naming
@@ -29006,12 +29594,6 @@ function isBlockElement(tagName) {
29006
29594
  function isVoidElement(tagName) {
29007
29595
  return HTML_VOID_ELEMENTS.has(tagName.toLowerCase());
29008
29596
  }
29009
- /**
29010
- * Checks if an attribute is a boolean attribute
29011
- */
29012
- function isBooleanAttribute(attributeName) {
29013
- return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
29014
- }
29015
29597
  /**
29016
29598
  * Attribute visitor that provides granular processing based on both
29017
29599
  * attribute name type (static/dynamic) and value type (static/dynamic)
@@ -29034,7 +29616,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
29034
29616
  forEachAttribute(node, (attributeNode) => {
29035
29617
  const staticAttributeName = getAttributeName(attributeNode);
29036
29618
  const originalAttributeName = getAttributeName(attributeNode, false) || "";
29037
- const isDynamicName = hasDynamicAttributeNameOnAttribute(attributeNode);
29619
+ const isDynamicName = hasDynamicAttributeName(attributeNode);
29038
29620
  const staticAttributeValue = getStaticAttributeValue(attributeNode);
29039
29621
  const valueNodes = getAttributeValueNodes(attributeNode);
29040
29622
  const hasOutputERB = hasERBOutput(valueNodes);
@@ -29111,7 +29693,7 @@ class BaseLexerRuleVisitor {
29111
29693
  * Helper method to create an unbound lint offense (without severity).
29112
29694
  * The Linter will bind severity based on the rule's config.
29113
29695
  */
29114
- createOffense(message, location, autofixContext, severity) {
29696
+ createOffense(message, location, autofixContext, severity, tags) {
29115
29697
  return {
29116
29698
  rule: this.ruleName,
29117
29699
  code: this.ruleName,
@@ -29120,13 +29702,14 @@ class BaseLexerRuleVisitor {
29120
29702
  location,
29121
29703
  autofixContext,
29122
29704
  severity,
29705
+ tags,
29123
29706
  };
29124
29707
  }
29125
29708
  /**
29126
29709
  * Helper method to add an offense to the offenses array
29127
29710
  */
29128
- addOffense(message, location, autofixContext, severity) {
29129
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29711
+ addOffense(message, location, autofixContext, severity, tags) {
29712
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
29130
29713
  }
29131
29714
  /**
29132
29715
  * Main entry point for lexer rule visitors
@@ -29167,7 +29750,7 @@ class BaseSourceRuleVisitor {
29167
29750
  * Helper method to create an unbound lint offense (without severity).
29168
29751
  * The Linter will bind severity based on the rule's config.
29169
29752
  */
29170
- createOffense(message, location, autofixContext, severity) {
29753
+ createOffense(message, location, autofixContext, severity, tags) {
29171
29754
  return {
29172
29755
  rule: this.ruleName,
29173
29756
  code: this.ruleName,
@@ -29176,13 +29759,14 @@ class BaseSourceRuleVisitor {
29176
29759
  location,
29177
29760
  autofixContext,
29178
29761
  severity,
29762
+ tags,
29179
29763
  };
29180
29764
  }
29181
29765
  /**
29182
29766
  * Helper method to add an offense to the offenses array
29183
29767
  */
29184
- addOffense(message, location, autofixContext, severity) {
29185
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29768
+ addOffense(message, location, autofixContext, severity, tags) {
29769
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
29186
29770
  }
29187
29771
  /**
29188
29772
  * Main entry point for source rule visitors
@@ -29532,6 +30116,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
29532
30116
  }
29533
30117
  }
29534
30118
 
30119
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
30120
+ visitERBRenderNode(node) {
30121
+ if (!isERBOutputNode(node)) {
30122
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
30123
+ }
30124
+ this.visitChildNodes(node);
30125
+ }
30126
+ }
30127
+ class ActionViewNoSilentRenderRule extends ParserRule {
30128
+ static ruleName = "actionview-no-silent-render";
30129
+ get defaultConfig() {
30130
+ return {
30131
+ enabled: true,
30132
+ severity: "error"
30133
+ };
30134
+ }
30135
+ get parserOptions() {
30136
+ return {
30137
+ render_nodes: true,
30138
+ };
30139
+ }
30140
+ check(result, context) {
30141
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
30142
+ visitor.visit(result.value);
30143
+ return visitor.offenses;
30144
+ }
30145
+ }
30146
+
29535
30147
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
29536
30148
  visitERBContentNode(node) {
29537
30149
  const content = node.content?.value || "";
@@ -29596,7 +30208,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
29596
30208
  for (const child of node.children) {
29597
30209
  if (!this.isAllowedContent(child)) {
29598
30210
  const childCode = IdentityPrinter.print(child).trim();
29599
- this.addOffense(`Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`, child.location);
30211
+ const offense = this.createOffense(`Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`, child.location);
30212
+ offense.tags = ["unnecessary"];
30213
+ this.offenses.push(offense);
29600
30214
  }
29601
30215
  }
29602
30216
  }
@@ -29626,6 +30240,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
29626
30240
  }
29627
30241
  }
29628
30242
 
30243
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
30244
+ processedIfNodes = new Set();
30245
+ processedElseNodes = new Set();
30246
+ visitERBIfNode(node) {
30247
+ if (this.processedIfNodes.has(node)) {
30248
+ return;
30249
+ }
30250
+ this.markIfChainAsProcessed(node);
30251
+ this.markElseNodesInIfChain(node);
30252
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
30253
+ if (entireChainEmpty) {
30254
+ this.addEmptyBlockOffense(node, node.statements, "if");
30255
+ }
30256
+ else {
30257
+ this.checkIfChainParts(node);
30258
+ }
30259
+ this.visitChildNodes(node);
30260
+ }
30261
+ visitERBElseNode(node) {
30262
+ if (this.processedElseNodes.has(node)) {
30263
+ this.visitChildNodes(node);
30264
+ return;
30265
+ }
30266
+ this.addEmptyBlockOffense(node, node.statements, "else");
30267
+ this.visitChildNodes(node);
30268
+ }
30269
+ visitERBUnlessNode(node) {
30270
+ const unlessHasContent = this.statementsHaveContent(node.statements);
30271
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
30272
+ if (node.else_clause) {
30273
+ this.processedElseNodes.add(node.else_clause);
30274
+ }
30275
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
30276
+ if (entireBlockEmpty) {
30277
+ this.addEmptyBlockOffense(node, node.statements, "unless");
30278
+ }
30279
+ else {
30280
+ if (!unlessHasContent) {
30281
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
30282
+ }
30283
+ if (node.else_clause && !elseHasContent) {
30284
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
30285
+ }
30286
+ }
30287
+ this.visitChildNodes(node);
30288
+ }
30289
+ visitERBForNode(node) {
30290
+ this.addEmptyBlockOffense(node, node.statements, "for");
30291
+ this.visitChildNodes(node);
30292
+ }
30293
+ visitERBWhileNode(node) {
30294
+ this.addEmptyBlockOffense(node, node.statements, "while");
30295
+ this.visitChildNodes(node);
30296
+ }
30297
+ visitERBUntilNode(node) {
30298
+ this.addEmptyBlockOffense(node, node.statements, "until");
30299
+ this.visitChildNodes(node);
30300
+ }
30301
+ visitERBWhenNode(node) {
30302
+ if (!node.then_keyword) {
30303
+ this.addEmptyBlockOffense(node, node.statements, "when");
30304
+ }
30305
+ this.visitChildNodes(node);
30306
+ }
30307
+ visitERBInNode(node) {
30308
+ if (!node.then_keyword) {
30309
+ this.addEmptyBlockOffense(node, node.statements, "in");
30310
+ }
30311
+ this.visitChildNodes(node);
30312
+ }
30313
+ visitERBBeginNode(node) {
30314
+ this.addEmptyBlockOffense(node, node.statements, "begin");
30315
+ this.visitChildNodes(node);
30316
+ }
30317
+ visitERBRescueNode(node) {
30318
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
30319
+ this.visitChildNodes(node);
30320
+ }
30321
+ visitERBEnsureNode(node) {
30322
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
30323
+ this.visitChildNodes(node);
30324
+ }
30325
+ visitERBBlockNode(node) {
30326
+ this.addEmptyBlockOffense(node, node.body, "do");
30327
+ this.visitChildNodes(node);
30328
+ }
30329
+ addEmptyBlockOffense(node, statements, blockType) {
30330
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
30331
+ }
30332
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
30333
+ if (this.statementsHaveContent(statements)) {
30334
+ return;
30335
+ }
30336
+ const startLocation = node.location.start;
30337
+ const endLocation = subsequentNode
30338
+ ? subsequentNode.location.start
30339
+ : node.location.end;
30340
+ const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
30341
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
30342
+ offense.tags = ["unnecessary"];
30343
+ this.offenses.push(offense);
30344
+ }
30345
+ statementsHaveContent(statements) {
30346
+ return statements.some(statement => {
30347
+ if (isHTMLTextNode(statement)) {
30348
+ return statement.content.trim() !== "";
30349
+ }
30350
+ return true;
30351
+ });
30352
+ }
30353
+ markIfChainAsProcessed(node) {
30354
+ this.processedIfNodes.add(node);
30355
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30356
+ if (isERBIfNode(current)) {
30357
+ this.processedIfNodes.add(current);
30358
+ }
30359
+ });
30360
+ }
30361
+ markElseNodesInIfChain(node) {
30362
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30363
+ if (isERBElseNode(current)) {
30364
+ this.processedElseNodes.add(current);
30365
+ }
30366
+ });
30367
+ }
30368
+ traverseSubsequentNodes(startNode, callback) {
30369
+ let current = startNode;
30370
+ while (current) {
30371
+ if (isERBIfNode(current)) {
30372
+ callback(current);
30373
+ current = current.subsequent;
30374
+ }
30375
+ else if (isERBElseNode(current)) {
30376
+ callback(current);
30377
+ break;
30378
+ }
30379
+ else {
30380
+ break;
30381
+ }
30382
+ }
30383
+ }
30384
+ checkIfChainParts(node) {
30385
+ if (!this.statementsHaveContent(node.statements)) {
30386
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
30387
+ }
30388
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30389
+ if (this.statementsHaveContent(current.statements)) {
30390
+ return;
30391
+ }
30392
+ const blockType = isERBIfNode(current) ? "elsif" : "else";
30393
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
30394
+ if (nextSubsequent) {
30395
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
30396
+ }
30397
+ else {
30398
+ this.addEmptyBlockOffense(current, current.statements, blockType);
30399
+ }
30400
+ });
30401
+ }
30402
+ isEntireIfChainEmpty(node) {
30403
+ if (this.statementsHaveContent(node.statements)) {
30404
+ return false;
30405
+ }
30406
+ let hasContent = false;
30407
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30408
+ if (this.statementsHaveContent(current.statements)) {
30409
+ hasContent = true;
30410
+ }
30411
+ });
30412
+ return !hasContent;
30413
+ }
30414
+ }
30415
+ class ERBNoEmptyControlFlowRule extends ParserRule {
30416
+ static ruleName = "erb-no-empty-control-flow";
30417
+ get defaultConfig() {
30418
+ return {
30419
+ enabled: true,
30420
+ severity: "hint"
30421
+ };
30422
+ }
30423
+ check(result, context) {
30424
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
30425
+ visitor.visit(result.value);
30426
+ return visitor.offenses;
30427
+ }
30428
+ }
30429
+
29629
30430
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
29630
30431
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
29631
30432
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
@@ -29784,6 +30585,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
29784
30585
  function getSignificantNodes(statements) {
29785
30586
  return statements.filter(node => !isPureWhitespaceNode(node));
29786
30587
  }
30588
+ function trimWhitespaceNodes(nodes) {
30589
+ let start = 0;
30590
+ let end = nodes.length;
30591
+ while (start < end && isPureWhitespaceNode(nodes[start]))
30592
+ start++;
30593
+ while (end > start && isPureWhitespaceNode(nodes[end - 1]))
30594
+ end--;
30595
+ return nodes.slice(start, end);
30596
+ }
29787
30597
  function allEquivalentElements(nodes) {
29788
30598
  if (nodes.length < 2)
29789
30599
  return false;
@@ -29901,9 +30711,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
29901
30711
  if (isERBIfNode(node)) {
29902
30712
  this.markSubsequentIfNodesAsProcessed(node);
29903
30713
  }
30714
+ if (this.allBranchesIdentical(branches)) {
30715
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
30716
+ return;
30717
+ }
29904
30718
  const state = { isFirstOffense: true };
29905
30719
  this.checkBranches(branches, node, state);
29906
30720
  }
30721
+ allBranchesIdentical(branches) {
30722
+ if (branches.length < 2)
30723
+ return false;
30724
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
30725
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
30726
+ }
29907
30727
  markSubsequentIfNodesAsProcessed(node) {
29908
30728
  let current = node.subsequent;
29909
30729
  while (current) {
@@ -29937,11 +30757,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
29937
30757
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
29938
30758
  for (const element of elements) {
29939
30759
  const printed = IdentityPrinter.print(element.open_tag);
29940
- const autofixContext = state.isFirstOffense
29941
- ? { node: conditionalNode }
29942
- : undefined;
29943
- this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, bodiesMatch ? element.location : (element?.open_tag?.location || element.location), autofixContext);
29944
- state.isFirstOffense = false;
30760
+ if (bodiesMatch) {
30761
+ const autofixContext = state.isFirstOffense
30762
+ ? { node: conditionalNode }
30763
+ : undefined;
30764
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
30765
+ state.isFirstOffense = false;
30766
+ }
30767
+ else {
30768
+ const autofixContext = state.isFirstOffense
30769
+ ? { node: conditionalNode }
30770
+ : undefined;
30771
+ const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
30772
+ ? element.open_tag.tag_name.location
30773
+ : element?.open_tag?.location || element.location;
30774
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
30775
+ state.isFirstOffense = false;
30776
+ }
29945
30777
  }
29946
30778
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
29947
30779
  this.checkBranches(bodies, conditionalNode, state);
@@ -29970,6 +30802,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
29970
30802
  const branches = collectBranches(conditionalNode);
29971
30803
  if (!branches)
29972
30804
  return null;
30805
+ if (offense.autofixContext.allIdentical) {
30806
+ const parentInfo = findParentArray(result.value, conditionalNode);
30807
+ if (!parentInfo)
30808
+ return null;
30809
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
30810
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
30811
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
30812
+ return result;
30813
+ }
29973
30814
  const significantBranches = branches.map(getSignificantNodes);
29974
30815
  if (significantBranches.some(branch => branch.length === 0))
29975
30816
  return null;
@@ -29983,23 +30824,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
29983
30824
  return null;
29984
30825
  let { array: parentArray, index: conditionalIndex } = parentInfo;
29985
30826
  let hasWrapped = false;
30827
+ let didMutate = false;
30828
+ let failedToHoistPrefix = false;
30829
+ let hoistedBefore = false;
29986
30830
  const hoistElement = (elements, position) => {
30831
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
29987
30832
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
29988
30833
  if (bodiesMatch) {
30834
+ if (actualPosition === "after") {
30835
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
30836
+ if (currentLengths.some(l => l !== currentLengths[0]))
30837
+ return;
30838
+ }
30839
+ if (actualPosition === "after" && position === "before") {
30840
+ const isAtEnd = branches.every((branch, index) => {
30841
+ const nodes = getSignificantNodes(branch);
30842
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
30843
+ });
30844
+ if (!isAtEnd)
30845
+ return;
30846
+ }
29989
30847
  for (let i = 0; i < branches.length; i++) {
29990
30848
  removeNodeFromArray(branches[i], elements[i]);
29991
30849
  }
29992
- if (position === "before") {
29993
- parentArray.splice(conditionalIndex, 0, elements[0]);
29994
- conditionalIndex++;
30850
+ if (actualPosition === "before") {
30851
+ parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
30852
+ conditionalIndex += 2;
30853
+ hoistedBefore = true;
29995
30854
  }
29996
30855
  else {
29997
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
30856
+ parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
29998
30857
  }
30858
+ didMutate = true;
29999
30859
  }
30000
30860
  else {
30001
30861
  if (hasWrapped)
30002
30862
  return;
30863
+ const canWrap = branches.every((branch, index) => {
30864
+ const remaining = getSignificantNodes(branch);
30865
+ return remaining.length === 1 && remaining[0] === elements[index];
30866
+ });
30867
+ if (!canWrap) {
30868
+ if (position === "before")
30869
+ failedToHoistPrefix = true;
30870
+ return;
30871
+ }
30003
30872
  for (let i = 0; i < branches.length; i++) {
30004
30873
  replaceNodeWithBody(branches[i], elements[i]);
30005
30874
  }
@@ -30008,6 +30877,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
30008
30877
  parentArray = wrapper.body;
30009
30878
  conditionalIndex = 1;
30010
30879
  hasWrapped = true;
30880
+ didMutate = true;
30011
30881
  }
30012
30882
  };
30013
30883
  for (let index = 0; index < prefixCount; index++) {
@@ -30018,7 +30888,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
30018
30888
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
30019
30889
  hoistElement(elements, "after");
30020
30890
  }
30021
- return result;
30891
+ if (!hasWrapped && hoistedBefore) {
30892
+ const remaining = branches.map(branch => getSignificantNodes(branch));
30893
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
30894
+ const elements = remaining.map(b => b[0]);
30895
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
30896
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
30897
+ for (let i = 0; i < branches.length; i++) {
30898
+ replaceNodeWithBody(branches[i], elements[i]);
30899
+ }
30900
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
30901
+ parentArray[conditionalIndex] = wrapper;
30902
+ didMutate = true;
30903
+ }
30904
+ }
30905
+ }
30906
+ return didMutate ? result : null;
30022
30907
  }
30023
30908
  }
30024
30909
 
@@ -30544,6 +31429,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
30544
31429
  }
30545
31430
  }
30546
31431
 
31432
+ function isAssignmentNode(prismNode) {
31433
+ const type = prismNode?.constructor?.name;
31434
+ if (!type)
31435
+ return false;
31436
+ return type.endsWith("WriteNode");
31437
+ }
31438
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
31439
+ visitERBContentNode(node) {
31440
+ if (isERBOutputNode(node))
31441
+ return;
31442
+ const prismNode = node.prismNode;
31443
+ if (!prismNode)
31444
+ return;
31445
+ if (isAssignmentNode(prismNode))
31446
+ return;
31447
+ const content = node.content?.value?.trim();
31448
+ if (!content)
31449
+ return;
31450
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
31451
+ }
31452
+ }
31453
+ class ERBNoSilentStatementRule extends ParserRule {
31454
+ static ruleName = "erb-no-silent-statement";
31455
+ get defaultConfig() {
31456
+ return {
31457
+ enabled: false,
31458
+ severity: "warning"
31459
+ };
31460
+ }
31461
+ get parserOptions() {
31462
+ return {
31463
+ prism_nodes: true,
31464
+ };
31465
+ }
31466
+ check(result, context) {
31467
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
31468
+ visitor.visit(result.value);
31469
+ return visitor.offenses;
31470
+ }
31471
+ }
31472
+
30547
31473
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
30548
31474
  visitHTMLAttributeNameNode(node) {
30549
31475
  const erbNodes = filterERBContentNodes(node.children);
@@ -30806,7 +31732,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
30806
31732
  }
30807
31733
 
30808
31734
  const JS_ATTRIBUTE_PATTERN = /^on/i;
30809
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
31735
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
30810
31736
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
30811
31737
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
30812
31738
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -30817,7 +31743,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
30817
31743
  if (!isERBOutputNode(node))
30818
31744
  continue;
30819
31745
  const content = node.content?.value?.trim() || "";
30820
- if (SAFE_PATTERN$1.test(content))
31746
+ if (SAFE_PATTERN.test(content))
30821
31747
  continue;
30822
31748
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
30823
31749
  }
@@ -30898,7 +31824,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
30898
31824
  }
30899
31825
  }
30900
31826
 
30901
- const SAFE_PATTERN = /\.to_json\b/;
31827
+ const SAFE_METHOD_NAMES = new Set([
31828
+ "to_json",
31829
+ "json_escape",
31830
+ ]);
31831
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
31832
+ "j",
31833
+ "escape_javascript",
31834
+ ]);
31835
+ class SafeCallDetector extends Visitor$1 {
31836
+ hasSafeCall = false;
31837
+ hasEscapeJavascriptCall = false;
31838
+ visitCallNode(node) {
31839
+ if (SAFE_METHOD_NAMES.has(node.name)) {
31840
+ this.hasSafeCall = true;
31841
+ }
31842
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
31843
+ this.hasEscapeJavascriptCall = true;
31844
+ }
31845
+ this.visitChildNodes(node);
31846
+ }
31847
+ }
30902
31848
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
30903
31849
  visitHTMLElementNode(node) {
30904
31850
  if (!isHTMLOpenTagNode(node.open_tag)) {
@@ -30927,9 +31873,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
30927
31873
  continue;
30928
31874
  if (!isERBOutputNode(child))
30929
31875
  continue;
30930
- const content = child.content?.value?.trim() || "";
30931
- if (SAFE_PATTERN.test(content))
31876
+ const erbContent = child;
31877
+ const prismNode = erbContent.prismNode;
31878
+ const detector = new SafeCallDetector();
31879
+ if (prismNode)
31880
+ detector.visit(prismNode);
31881
+ if (detector.hasSafeCall)
31882
+ continue;
31883
+ if (detector.hasEscapeJavascriptCall) {
31884
+ this.addOffense("Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position.", child.location);
30932
31885
  continue;
31886
+ }
30933
31887
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
30934
31888
  }
30935
31889
  }
@@ -30942,6 +31896,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
30942
31896
  severity: "error"
30943
31897
  };
30944
31898
  }
31899
+ get parserOptions() {
31900
+ return {
31901
+ prism_nodes: true,
31902
+ };
31903
+ }
30945
31904
  check(result, context) {
30946
31905
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
30947
31906
  visitor.visit(result.value);
@@ -31949,7 +32908,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
31949
32908
  }
31950
32909
  }
31951
32910
 
31952
- const ALLOWED_TYPES = ["text/javascript"];
32911
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
31953
32912
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
31954
32913
  visitHTMLOpenTagNode(node) {
31955
32914
  if (getTagLocalName(node) === "script") {
@@ -32487,6 +33446,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
32487
33446
  }
32488
33447
  }
32489
33448
 
33449
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
33450
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
33451
+ this.checkAttribute(originalAttributeName, attributeNode);
33452
+ }
33453
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
33454
+ this.checkAttribute(originalAttributeName, attributeNode);
33455
+ }
33456
+ checkAttribute(attributeName, attributeNode) {
33457
+ if (!isBooleanAttribute(attributeName))
33458
+ return;
33459
+ if (!hasAttributeValue(attributeNode))
33460
+ return;
33461
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
33462
+ node: attributeNode
33463
+ });
33464
+ }
33465
+ }
33466
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
33467
+ static autocorrectable = true;
33468
+ static ruleName = "html-boolean-attributes-no-value";
33469
+ get defaultConfig() {
33470
+ return {
33471
+ enabled: true,
33472
+ severity: "error"
33473
+ };
33474
+ }
33475
+ check(result, context) {
33476
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
33477
+ visitor.visit(result.value);
33478
+ return visitor.offenses;
33479
+ }
33480
+ autofix(offense, result, _context) {
33481
+ if (!offense.autofixContext)
33482
+ return null;
33483
+ const { node } = offense.autofixContext;
33484
+ node.equals = null;
33485
+ node.value = null;
33486
+ return result;
33487
+ }
33488
+ }
33489
+
32490
33490
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
32491
33491
  visitHTMLElementNode(node) {
32492
33492
  this.checkDetailsElement(node);
@@ -32536,47 +33536,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
32536
33536
  }
32537
33537
  }
32538
33538
 
32539
- class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
32540
- checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
32541
- this.checkAttribute(originalAttributeName, attributeNode);
32542
- }
32543
- checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
32544
- this.checkAttribute(originalAttributeName, attributeNode);
32545
- }
32546
- checkAttribute(attributeName, attributeNode) {
32547
- if (!isBooleanAttribute(attributeName))
32548
- return;
32549
- if (!hasAttributeValue(attributeNode))
32550
- return;
32551
- this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
32552
- node: attributeNode
32553
- });
32554
- }
32555
- }
32556
- class HTMLBooleanAttributesNoValueRule extends ParserRule {
32557
- static autocorrectable = true;
32558
- static ruleName = "html-boolean-attributes-no-value";
32559
- get defaultConfig() {
32560
- return {
32561
- enabled: true,
32562
- severity: "error"
32563
- };
32564
- }
32565
- check(result, context) {
32566
- const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
32567
- visitor.visit(result.value);
32568
- return visitor.offenses;
32569
- }
32570
- autofix(offense, result, _context) {
32571
- if (!offense.autofixContext)
32572
- return null;
32573
- const { node } = offense.autofixContext;
32574
- node.equals = null;
32575
- node.value = null;
32576
- return result;
32577
- }
32578
- }
32579
-
32580
33539
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
32581
33540
  elementStack = [];
32582
33541
  visitHTMLElementNode(node) {
@@ -34259,8 +35218,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
34259
35218
 
34260
35219
  const rules = [
34261
35220
  ActionViewNoSilentHelperRule,
35221
+ ActionViewNoSilentRenderRule,
34262
35222
  ERBCommentSyntax,
34263
35223
  ERBNoCaseNodeChildrenRule,
35224
+ ERBNoEmptyControlFlowRule,
34264
35225
  ERBNoConditionalHTMLElementRule,
34265
35226
  ERBNoConditionalOpenTagRule,
34266
35227
  ERBNoDuplicateBranchElementsRule,
@@ -34275,6 +35236,7 @@ const rules = [
34275
35236
  ERBNoOutputInAttributeNameRule,
34276
35237
  ERBNoOutputInAttributePositionRule,
34277
35238
  ERBNoRawOutputInAttributeValueRule,
35239
+ ERBNoSilentStatementRule,
34278
35240
  ERBNoSilentTagInAttributeNameRule,
34279
35241
  ERBNoStatementInScriptRule,
34280
35242
  ERBNoThenInControlFlowRule,
@@ -34306,8 +35268,8 @@ const rules = [
34306
35268
  HTMLAttributeValuesRequireQuotesRule,
34307
35269
  HTMLAvoidBothDisabledAndAriaDisabledRule,
34308
35270
  HTMLBodyOnlyElementsRule,
34309
- HTMLDetailsHasSummaryRule,
34310
35271
  HTMLBooleanAttributesNoValueRule,
35272
+ HTMLDetailsHasSummaryRule,
34311
35273
  HTMLHeadOnlyElementsRule,
34312
35274
  HTMLIframeHasTitleRule,
34313
35275
  HTMLImgRequireAltRule,
@@ -35882,5 +36844,5 @@ async function loadCustomRules(options) {
35882
36844
  };
35883
36845
  }
35884
36846
 
35885
- export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, CustomRuleLoader, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, STRICT_LOCALS_PATTERN, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findNodeAtPosition, findNodeByLocation, findParent, getBasename, hasBalancedParentheses, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, loadCustomRules, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
36847
+ export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, ActionViewNoSilentHelperRule, ActionViewNoSilentRenderRule, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, CustomRuleLoader, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyControlFlowRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentStatementRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, STRICT_LOCALS_PATTERN, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findAttributeByName, findNodeAtPosition, findNodeByLocation, findParent, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getBasename, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasBalancedParentheses, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, loadCustomRules, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
35886
36848
  //# sourceMappingURL=loader.js.map