@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.cjs CHANGED
@@ -2327,7 +2327,7 @@ class Token {
2327
2327
  }
2328
2328
 
2329
2329
  // NOTE: This file is generated by the templates/template.rb script and should not
2330
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/errors.ts.erb
2330
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/errors.ts.erb
2331
2331
  class HerbError {
2332
2332
  type;
2333
2333
  message;
@@ -3328,6 +3328,279 @@ class NestedERBTagError extends HerbError {
3328
3328
  return output;
3329
3329
  }
3330
3330
  }
3331
+ class RenderAmbiguousLocalsError extends HerbError {
3332
+ partial;
3333
+ static from(data) {
3334
+ return new RenderAmbiguousLocalsError({
3335
+ type: data.type,
3336
+ message: data.message,
3337
+ location: Location.from(data.location),
3338
+ partial: data.partial,
3339
+ });
3340
+ }
3341
+ constructor(props) {
3342
+ super(props.type, props.message, props.location);
3343
+ this.partial = props.partial;
3344
+ }
3345
+ toJSON() {
3346
+ return {
3347
+ ...super.toJSON(),
3348
+ type: "RENDER_AMBIGUOUS_LOCALS_ERROR",
3349
+ partial: this.partial,
3350
+ };
3351
+ }
3352
+ toMonacoDiagnostic() {
3353
+ return {
3354
+ line: this.location.start.line,
3355
+ column: this.location.start.column,
3356
+ endLine: this.location.end.line,
3357
+ endColumn: this.location.end.column,
3358
+ message: this.message,
3359
+ severity: 'error'
3360
+ };
3361
+ }
3362
+ treeInspect() {
3363
+ let output = "";
3364
+ output += `@ RenderAmbiguousLocalsError ${this.location.treeInspectWithLabel()}\n`;
3365
+ output += `├── message: "${this.message}"\n`;
3366
+ output += `└── partial: ${JSON.stringify(this.partial)}\n`;
3367
+ return output;
3368
+ }
3369
+ }
3370
+ class RenderMissingLocalsError extends HerbError {
3371
+ partial;
3372
+ keywords;
3373
+ static from(data) {
3374
+ return new RenderMissingLocalsError({
3375
+ type: data.type,
3376
+ message: data.message,
3377
+ location: Location.from(data.location),
3378
+ partial: data.partial,
3379
+ keywords: data.keywords,
3380
+ });
3381
+ }
3382
+ constructor(props) {
3383
+ super(props.type, props.message, props.location);
3384
+ this.partial = props.partial;
3385
+ this.keywords = props.keywords;
3386
+ }
3387
+ toJSON() {
3388
+ return {
3389
+ ...super.toJSON(),
3390
+ type: "RENDER_MISSING_LOCALS_ERROR",
3391
+ partial: this.partial,
3392
+ keywords: this.keywords,
3393
+ };
3394
+ }
3395
+ toMonacoDiagnostic() {
3396
+ return {
3397
+ line: this.location.start.line,
3398
+ column: this.location.start.column,
3399
+ endLine: this.location.end.line,
3400
+ endColumn: this.location.end.column,
3401
+ message: this.message,
3402
+ severity: 'error'
3403
+ };
3404
+ }
3405
+ treeInspect() {
3406
+ let output = "";
3407
+ output += `@ RenderMissingLocalsError ${this.location.treeInspectWithLabel()}\n`;
3408
+ output += `├── message: "${this.message}"\n`;
3409
+ output += `├── partial: ${JSON.stringify(this.partial)}\n`;
3410
+ output += `└── keywords: ${JSON.stringify(this.keywords)}\n`;
3411
+ return output;
3412
+ }
3413
+ }
3414
+ class RenderNoArgumentsError extends HerbError {
3415
+ static from(data) {
3416
+ return new RenderNoArgumentsError({
3417
+ type: data.type,
3418
+ message: data.message,
3419
+ location: Location.from(data.location),
3420
+ });
3421
+ }
3422
+ constructor(props) {
3423
+ super(props.type, props.message, props.location);
3424
+ }
3425
+ toJSON() {
3426
+ return {
3427
+ ...super.toJSON(),
3428
+ type: "RENDER_NO_ARGUMENTS_ERROR",
3429
+ };
3430
+ }
3431
+ toMonacoDiagnostic() {
3432
+ return {
3433
+ line: this.location.start.line,
3434
+ column: this.location.start.column,
3435
+ endLine: this.location.end.line,
3436
+ endColumn: this.location.end.column,
3437
+ message: this.message,
3438
+ severity: 'error'
3439
+ };
3440
+ }
3441
+ treeInspect() {
3442
+ let output = "";
3443
+ output += `@ RenderNoArgumentsError ${this.location.treeInspectWithLabel()}\n`;
3444
+ output += `└── message: "${this.message}"\n`;
3445
+ return output;
3446
+ }
3447
+ }
3448
+ class RenderConflictingPartialError extends HerbError {
3449
+ positional_partial;
3450
+ keyword_partial;
3451
+ static from(data) {
3452
+ return new RenderConflictingPartialError({
3453
+ type: data.type,
3454
+ message: data.message,
3455
+ location: Location.from(data.location),
3456
+ positional_partial: data.positional_partial,
3457
+ keyword_partial: data.keyword_partial,
3458
+ });
3459
+ }
3460
+ constructor(props) {
3461
+ super(props.type, props.message, props.location);
3462
+ this.positional_partial = props.positional_partial;
3463
+ this.keyword_partial = props.keyword_partial;
3464
+ }
3465
+ toJSON() {
3466
+ return {
3467
+ ...super.toJSON(),
3468
+ type: "RENDER_CONFLICTING_PARTIAL_ERROR",
3469
+ positional_partial: this.positional_partial,
3470
+ keyword_partial: this.keyword_partial,
3471
+ };
3472
+ }
3473
+ toMonacoDiagnostic() {
3474
+ return {
3475
+ line: this.location.start.line,
3476
+ column: this.location.start.column,
3477
+ endLine: this.location.end.line,
3478
+ endColumn: this.location.end.column,
3479
+ message: this.message,
3480
+ severity: 'error'
3481
+ };
3482
+ }
3483
+ treeInspect() {
3484
+ let output = "";
3485
+ output += `@ RenderConflictingPartialError ${this.location.treeInspectWithLabel()}\n`;
3486
+ output += `├── message: "${this.message}"\n`;
3487
+ output += `├── positional_partial: ${JSON.stringify(this.positional_partial)}\n`;
3488
+ output += `└── keyword_partial: ${JSON.stringify(this.keyword_partial)}\n`;
3489
+ return output;
3490
+ }
3491
+ }
3492
+ class RenderInvalidAsOptionError extends HerbError {
3493
+ as_value;
3494
+ static from(data) {
3495
+ return new RenderInvalidAsOptionError({
3496
+ type: data.type,
3497
+ message: data.message,
3498
+ location: Location.from(data.location),
3499
+ as_value: data.as_value,
3500
+ });
3501
+ }
3502
+ constructor(props) {
3503
+ super(props.type, props.message, props.location);
3504
+ this.as_value = props.as_value;
3505
+ }
3506
+ toJSON() {
3507
+ return {
3508
+ ...super.toJSON(),
3509
+ type: "RENDER_INVALID_AS_OPTION_ERROR",
3510
+ as_value: this.as_value,
3511
+ };
3512
+ }
3513
+ toMonacoDiagnostic() {
3514
+ return {
3515
+ line: this.location.start.line,
3516
+ column: this.location.start.column,
3517
+ endLine: this.location.end.line,
3518
+ endColumn: this.location.end.column,
3519
+ message: this.message,
3520
+ severity: 'error'
3521
+ };
3522
+ }
3523
+ treeInspect() {
3524
+ let output = "";
3525
+ output += `@ RenderInvalidAsOptionError ${this.location.treeInspectWithLabel()}\n`;
3526
+ output += `├── message: "${this.message}"\n`;
3527
+ output += `└── as_value: ${JSON.stringify(this.as_value)}\n`;
3528
+ return output;
3529
+ }
3530
+ }
3531
+ class RenderObjectAndCollectionError extends HerbError {
3532
+ static from(data) {
3533
+ return new RenderObjectAndCollectionError({
3534
+ type: data.type,
3535
+ message: data.message,
3536
+ location: Location.from(data.location),
3537
+ });
3538
+ }
3539
+ constructor(props) {
3540
+ super(props.type, props.message, props.location);
3541
+ }
3542
+ toJSON() {
3543
+ return {
3544
+ ...super.toJSON(),
3545
+ type: "RENDER_OBJECT_AND_COLLECTION_ERROR",
3546
+ };
3547
+ }
3548
+ toMonacoDiagnostic() {
3549
+ return {
3550
+ line: this.location.start.line,
3551
+ column: this.location.start.column,
3552
+ endLine: this.location.end.line,
3553
+ endColumn: this.location.end.column,
3554
+ message: this.message,
3555
+ severity: 'error'
3556
+ };
3557
+ }
3558
+ treeInspect() {
3559
+ let output = "";
3560
+ output += `@ RenderObjectAndCollectionError ${this.location.treeInspectWithLabel()}\n`;
3561
+ output += `└── message: "${this.message}"\n`;
3562
+ return output;
3563
+ }
3564
+ }
3565
+ class RenderLayoutWithoutBlockError extends HerbError {
3566
+ layout;
3567
+ static from(data) {
3568
+ return new RenderLayoutWithoutBlockError({
3569
+ type: data.type,
3570
+ message: data.message,
3571
+ location: Location.from(data.location),
3572
+ layout: data.layout,
3573
+ });
3574
+ }
3575
+ constructor(props) {
3576
+ super(props.type, props.message, props.location);
3577
+ this.layout = props.layout;
3578
+ }
3579
+ toJSON() {
3580
+ return {
3581
+ ...super.toJSON(),
3582
+ type: "RENDER_LAYOUT_WITHOUT_BLOCK_ERROR",
3583
+ layout: this.layout,
3584
+ };
3585
+ }
3586
+ toMonacoDiagnostic() {
3587
+ return {
3588
+ line: this.location.start.line,
3589
+ column: this.location.start.column,
3590
+ endLine: this.location.end.line,
3591
+ endColumn: this.location.end.column,
3592
+ message: this.message,
3593
+ severity: 'error'
3594
+ };
3595
+ }
3596
+ treeInspect() {
3597
+ let output = "";
3598
+ output += `@ RenderLayoutWithoutBlockError ${this.location.treeInspectWithLabel()}\n`;
3599
+ output += `├── message: "${this.message}"\n`;
3600
+ output += `└── layout: ${JSON.stringify(this.layout)}\n`;
3601
+ return output;
3602
+ }
3603
+ }
3331
3604
  function fromSerializedError(error) {
3332
3605
  switch (error.type) {
3333
3606
  case "UNEXPECTED_ERROR": return UnexpectedError.from(error);
@@ -3353,6 +3626,13 @@ function fromSerializedError(error) {
3353
3626
  case "UNCLOSED_ERB_TAG_ERROR": return UnclosedERBTagError.from(error);
3354
3627
  case "STRAY_ERB_CLOSING_TAG_ERROR": return StrayERBClosingTagError.from(error);
3355
3628
  case "NESTED_ERB_TAG_ERROR": return NestedERBTagError.from(error);
3629
+ case "RENDER_AMBIGUOUS_LOCALS_ERROR": return RenderAmbiguousLocalsError.from(error);
3630
+ case "RENDER_MISSING_LOCALS_ERROR": return RenderMissingLocalsError.from(error);
3631
+ case "RENDER_NO_ARGUMENTS_ERROR": return RenderNoArgumentsError.from(error);
3632
+ case "RENDER_CONFLICTING_PARTIAL_ERROR": return RenderConflictingPartialError.from(error);
3633
+ case "RENDER_INVALID_AS_OPTION_ERROR": return RenderInvalidAsOptionError.from(error);
3634
+ case "RENDER_OBJECT_AND_COLLECTION_ERROR": return RenderObjectAndCollectionError.from(error);
3635
+ case "RENDER_LAYOUT_WITHOUT_BLOCK_ERROR": return RenderLayoutWithoutBlockError.from(error);
3356
3636
  default:
3357
3637
  throw new Error(`Unknown node type: ${error.type}`);
3358
3638
  }
@@ -23368,7 +23648,7 @@ function deserializePrismNode(bytes, source) {
23368
23648
  }
23369
23649
 
23370
23650
  // NOTE: This file is generated by the templates/template.rb script and should not
23371
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/nodes.ts.erb
23651
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/nodes.ts.erb
23372
23652
  class Node {
23373
23653
  type;
23374
23654
  location;
@@ -25912,6 +26192,225 @@ class ERBUnlessNode extends Node {
25912
26192
  return output;
25913
26193
  }
25914
26194
  }
26195
+ class RubyRenderLocalNode extends Node {
26196
+ name;
26197
+ value;
26198
+ static get type() {
26199
+ return "AST_RUBY_RENDER_LOCAL_NODE";
26200
+ }
26201
+ static from(data) {
26202
+ return new RubyRenderLocalNode({
26203
+ type: data.type,
26204
+ location: Location.from(data.location),
26205
+ errors: (data.errors || []).map(error => HerbError.from(error)),
26206
+ name: data.name ? Token.from(data.name) : null,
26207
+ value: data.value ? fromSerializedNode((data.value)) : null,
26208
+ });
26209
+ }
26210
+ constructor(props) {
26211
+ super(props.type, props.location, props.errors);
26212
+ this.name = props.name;
26213
+ this.value = props.value;
26214
+ }
26215
+ accept(visitor) {
26216
+ visitor.visitRubyRenderLocalNode(this);
26217
+ }
26218
+ childNodes() {
26219
+ return [
26220
+ this.value,
26221
+ ];
26222
+ }
26223
+ compactChildNodes() {
26224
+ return this.childNodes().filter(node => node !== null && node !== undefined);
26225
+ }
26226
+ recursiveErrors() {
26227
+ return [
26228
+ ...this.errors,
26229
+ this.value ? this.value.recursiveErrors() : [],
26230
+ ].flat();
26231
+ }
26232
+ toJSON() {
26233
+ return {
26234
+ ...super.toJSON(),
26235
+ type: "AST_RUBY_RENDER_LOCAL_NODE",
26236
+ name: this.name ? this.name.toJSON() : null,
26237
+ value: this.value ? this.value.toJSON() : null,
26238
+ };
26239
+ }
26240
+ treeInspect() {
26241
+ let output = "";
26242
+ output += `@ RubyRenderLocalNode ${this.location.treeInspectWithLabel()}\n`;
26243
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
26244
+ output += `├── name: ${this.name ? this.name.treeInspect() : "∅"}\n`;
26245
+ output += `└── value: ${this.inspectNode(this.value, " ")}`;
26246
+ return output;
26247
+ }
26248
+ }
26249
+ class ERBRenderNode extends Node {
26250
+ tag_opening;
26251
+ content;
26252
+ tag_closing;
26253
+ // no-op for analyzed_ruby
26254
+ prism_node;
26255
+ partial;
26256
+ template_path;
26257
+ layout;
26258
+ file;
26259
+ inline_template;
26260
+ body;
26261
+ plain;
26262
+ html;
26263
+ renderable;
26264
+ collection;
26265
+ object;
26266
+ as_name;
26267
+ spacer_template;
26268
+ formats;
26269
+ variants;
26270
+ handlers;
26271
+ content_type;
26272
+ locals;
26273
+ static get type() {
26274
+ return "AST_ERB_RENDER_NODE";
26275
+ }
26276
+ static from(data) {
26277
+ return new ERBRenderNode({
26278
+ type: data.type,
26279
+ location: Location.from(data.location),
26280
+ errors: (data.errors || []).map(error => HerbError.from(error)),
26281
+ tag_opening: data.tag_opening ? Token.from(data.tag_opening) : null,
26282
+ content: data.content ? Token.from(data.content) : null,
26283
+ tag_closing: data.tag_closing ? Token.from(data.tag_closing) : null,
26284
+ // no-op for analyzed_ruby
26285
+ prism_node: data.prism_node ? new Uint8Array(data.prism_node) : null,
26286
+ partial: data.partial ? Token.from(data.partial) : null,
26287
+ template_path: data.template_path ? Token.from(data.template_path) : null,
26288
+ layout: data.layout ? Token.from(data.layout) : null,
26289
+ file: data.file ? Token.from(data.file) : null,
26290
+ inline_template: data.inline_template ? Token.from(data.inline_template) : null,
26291
+ body: data.body ? Token.from(data.body) : null,
26292
+ plain: data.plain ? Token.from(data.plain) : null,
26293
+ html: data.html ? Token.from(data.html) : null,
26294
+ renderable: data.renderable ? Token.from(data.renderable) : null,
26295
+ collection: data.collection ? Token.from(data.collection) : null,
26296
+ object: data.object ? Token.from(data.object) : null,
26297
+ as_name: data.as_name ? Token.from(data.as_name) : null,
26298
+ spacer_template: data.spacer_template ? Token.from(data.spacer_template) : null,
26299
+ formats: data.formats ? Token.from(data.formats) : null,
26300
+ variants: data.variants ? Token.from(data.variants) : null,
26301
+ handlers: data.handlers ? Token.from(data.handlers) : null,
26302
+ content_type: data.content_type ? Token.from(data.content_type) : null,
26303
+ locals: (data.locals || []).map(node => fromSerializedNode(node)),
26304
+ });
26305
+ }
26306
+ constructor(props) {
26307
+ super(props.type, props.location, props.errors);
26308
+ this.tag_opening = props.tag_opening;
26309
+ this.content = props.content;
26310
+ this.tag_closing = props.tag_closing;
26311
+ // no-op for analyzed_ruby
26312
+ this.prism_node = props.prism_node;
26313
+ this.partial = props.partial;
26314
+ this.template_path = props.template_path;
26315
+ this.layout = props.layout;
26316
+ this.file = props.file;
26317
+ this.inline_template = props.inline_template;
26318
+ this.body = props.body;
26319
+ this.plain = props.plain;
26320
+ this.html = props.html;
26321
+ this.renderable = props.renderable;
26322
+ this.collection = props.collection;
26323
+ this.object = props.object;
26324
+ this.as_name = props.as_name;
26325
+ this.spacer_template = props.spacer_template;
26326
+ this.formats = props.formats;
26327
+ this.variants = props.variants;
26328
+ this.handlers = props.handlers;
26329
+ this.content_type = props.content_type;
26330
+ this.locals = props.locals;
26331
+ }
26332
+ accept(visitor) {
26333
+ visitor.visitERBRenderNode(this);
26334
+ }
26335
+ childNodes() {
26336
+ return [
26337
+ ...this.locals,
26338
+ ];
26339
+ }
26340
+ compactChildNodes() {
26341
+ return this.childNodes().filter(node => node !== null && node !== undefined);
26342
+ }
26343
+ get prismNode() {
26344
+ if (!this.prism_node || !this.source)
26345
+ return null;
26346
+ return deserializePrismNode(this.prism_node, this.source);
26347
+ }
26348
+ recursiveErrors() {
26349
+ return [
26350
+ ...this.errors,
26351
+ ...this.locals.map(node => node.recursiveErrors()),
26352
+ ].flat();
26353
+ }
26354
+ toJSON() {
26355
+ return {
26356
+ ...super.toJSON(),
26357
+ type: "AST_ERB_RENDER_NODE",
26358
+ tag_opening: this.tag_opening ? this.tag_opening.toJSON() : null,
26359
+ content: this.content ? this.content.toJSON() : null,
26360
+ tag_closing: this.tag_closing ? this.tag_closing.toJSON() : null,
26361
+ // no-op for analyzed_ruby
26362
+ prism_node: this.prism_node ? Array.from(this.prism_node) : null,
26363
+ partial: this.partial ? this.partial.toJSON() : null,
26364
+ template_path: this.template_path ? this.template_path.toJSON() : null,
26365
+ layout: this.layout ? this.layout.toJSON() : null,
26366
+ file: this.file ? this.file.toJSON() : null,
26367
+ inline_template: this.inline_template ? this.inline_template.toJSON() : null,
26368
+ body: this.body ? this.body.toJSON() : null,
26369
+ plain: this.plain ? this.plain.toJSON() : null,
26370
+ html: this.html ? this.html.toJSON() : null,
26371
+ renderable: this.renderable ? this.renderable.toJSON() : null,
26372
+ collection: this.collection ? this.collection.toJSON() : null,
26373
+ object: this.object ? this.object.toJSON() : null,
26374
+ as_name: this.as_name ? this.as_name.toJSON() : null,
26375
+ spacer_template: this.spacer_template ? this.spacer_template.toJSON() : null,
26376
+ formats: this.formats ? this.formats.toJSON() : null,
26377
+ variants: this.variants ? this.variants.toJSON() : null,
26378
+ handlers: this.handlers ? this.handlers.toJSON() : null,
26379
+ content_type: this.content_type ? this.content_type.toJSON() : null,
26380
+ locals: this.locals.map(node => node.toJSON()),
26381
+ };
26382
+ }
26383
+ treeInspect() {
26384
+ let output = "";
26385
+ output += `@ ERBRenderNode ${this.location.treeInspectWithLabel()}\n`;
26386
+ output += `├── errors: ${this.inspectArray(this.errors, "│ ")}`;
26387
+ output += `├── tag_opening: ${this.tag_opening ? this.tag_opening.treeInspect() : "∅"}\n`;
26388
+ output += `├── content: ${this.content ? this.content.treeInspect() : "∅"}\n`;
26389
+ output += `├── tag_closing: ${this.tag_closing ? this.tag_closing.treeInspect() : "∅"}\n`;
26390
+ if (this.prism_node) {
26391
+ output += `├── prism_node: ${this.source ? inspectPrismSerialized(this.prism_node, this.source, "│ ") : `(${this.prism_node.length} bytes)`}\n`;
26392
+ }
26393
+ output += `├── partial: ${this.partial ? this.partial.treeInspect() : "∅"}\n`;
26394
+ output += `├── template_path: ${this.template_path ? this.template_path.treeInspect() : "∅"}\n`;
26395
+ output += `├── layout: ${this.layout ? this.layout.treeInspect() : "∅"}\n`;
26396
+ output += `├── file: ${this.file ? this.file.treeInspect() : "∅"}\n`;
26397
+ output += `├── inline_template: ${this.inline_template ? this.inline_template.treeInspect() : "∅"}\n`;
26398
+ output += `├── body: ${this.body ? this.body.treeInspect() : "∅"}\n`;
26399
+ output += `├── plain: ${this.plain ? this.plain.treeInspect() : "∅"}\n`;
26400
+ output += `├── html: ${this.html ? this.html.treeInspect() : "∅"}\n`;
26401
+ output += `├── renderable: ${this.renderable ? this.renderable.treeInspect() : "∅"}\n`;
26402
+ output += `├── collection: ${this.collection ? this.collection.treeInspect() : "∅"}\n`;
26403
+ output += `├── object: ${this.object ? this.object.treeInspect() : "∅"}\n`;
26404
+ output += `├── as_name: ${this.as_name ? this.as_name.treeInspect() : "∅"}\n`;
26405
+ output += `├── spacer_template: ${this.spacer_template ? this.spacer_template.treeInspect() : "∅"}\n`;
26406
+ output += `├── formats: ${this.formats ? this.formats.treeInspect() : "∅"}\n`;
26407
+ output += `├── variants: ${this.variants ? this.variants.treeInspect() : "∅"}\n`;
26408
+ output += `├── handlers: ${this.handlers ? this.handlers.treeInspect() : "∅"}\n`;
26409
+ output += `├── content_type: ${this.content_type ? this.content_type.treeInspect() : "∅"}\n`;
26410
+ output += `└── locals: ${this.inspectArray(this.locals, " ")}`;
26411
+ return output;
26412
+ }
26413
+ }
25915
26414
  class ERBYieldNode extends Node {
25916
26415
  tag_opening;
25917
26416
  content;
@@ -26075,6 +26574,8 @@ function fromSerializedNode(node) {
26075
26574
  case "AST_ERB_ENSURE_NODE": return ERBEnsureNode.from(node);
26076
26575
  case "AST_ERB_BEGIN_NODE": return ERBBeginNode.from(node);
26077
26576
  case "AST_ERB_UNLESS_NODE": return ERBUnlessNode.from(node);
26577
+ case "AST_RUBY_RENDER_LOCAL_NODE": return RubyRenderLocalNode.from(node);
26578
+ case "AST_ERB_RENDER_NODE": return ERBRenderNode.from(node);
26078
26579
  case "AST_ERB_YIELD_NODE": return ERBYieldNode.from(node);
26079
26580
  case "AST_ERB_IN_NODE": return ERBInNode.from(node);
26080
26581
  default:
@@ -26124,6 +26625,7 @@ const DEFAULT_PARSER_OPTIONS = {
26124
26625
  analyze: true,
26125
26626
  strict: true,
26126
26627
  action_view_helpers: false,
26628
+ render_nodes: false,
26127
26629
  prism_nodes: false,
26128
26630
  prism_nodes_deep: false,
26129
26631
  prism_program: false,
@@ -26140,6 +26642,8 @@ class ParserOptions {
26140
26642
  analyze;
26141
26643
  /** Whether ActionView tag helper transformation was enabled during parsing. */
26142
26644
  action_view_helpers;
26645
+ /** Whether ActionView render call detection was enabled during parsing. */
26646
+ render_nodes;
26143
26647
  /** Whether Prism node serialization was enabled during parsing. */
26144
26648
  prism_nodes;
26145
26649
  /** Whether deep Prism node serialization was enabled during parsing. */
@@ -26154,6 +26658,7 @@ class ParserOptions {
26154
26658
  this.track_whitespace = options.track_whitespace ?? DEFAULT_PARSER_OPTIONS.track_whitespace;
26155
26659
  this.analyze = options.analyze ?? DEFAULT_PARSER_OPTIONS.analyze;
26156
26660
  this.action_view_helpers = options.action_view_helpers ?? DEFAULT_PARSER_OPTIONS.action_view_helpers;
26661
+ this.render_nodes = options.render_nodes ?? DEFAULT_PARSER_OPTIONS.render_nodes;
26157
26662
  this.prism_nodes = options.prism_nodes ?? DEFAULT_PARSER_OPTIONS.prism_nodes;
26158
26663
  this.prism_nodes_deep = options.prism_nodes_deep ?? DEFAULT_PARSER_OPTIONS.prism_nodes_deep;
26159
26664
  this.prism_program = options.prism_program ?? DEFAULT_PARSER_OPTIONS.prism_program;
@@ -26233,7 +26738,7 @@ class ParseResult extends Result {
26233
26738
  }
26234
26739
 
26235
26740
  // NOTE: This file is generated by the templates/template.rb script and should not
26236
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/node-type-guards.ts.erb
26741
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/node-type-guards.ts.erb
26237
26742
  /**
26238
26743
  * Type guard functions for AST nodes.
26239
26744
  * These functions provide type checking by combining both instanceof
@@ -26528,6 +27033,22 @@ function isERBUnlessNode(node) {
26528
27033
  return false;
26529
27034
  return node instanceof ERBUnlessNode || node.type === "AST_ERB_UNLESS_NODE" || node.constructor.type === "AST_ERB_UNLESS_NODE";
26530
27035
  }
27036
+ /**
27037
+ * Checks if a node is a RubyRenderLocalNode
27038
+ */
27039
+ function isRubyRenderLocalNode(node) {
27040
+ if (!node)
27041
+ return false;
27042
+ return node instanceof RubyRenderLocalNode || node.type === "AST_RUBY_RENDER_LOCAL_NODE" || node.constructor.type === "AST_RUBY_RENDER_LOCAL_NODE";
27043
+ }
27044
+ /**
27045
+ * Checks if a node is a ERBRenderNode
27046
+ */
27047
+ function isERBRenderNode(node) {
27048
+ if (!node)
27049
+ return false;
27050
+ return node instanceof ERBRenderNode || node.type === "AST_ERB_RENDER_NODE" || node.constructor.type === "AST_ERB_RENDER_NODE";
27051
+ }
26531
27052
  /**
26532
27053
  * Checks if a node is a ERBYieldNode
26533
27054
  */
@@ -26564,6 +27085,7 @@ function isERBNode(node) {
26564
27085
  isERBEnsureNode(node) ||
26565
27086
  isERBBeginNode(node) ||
26566
27087
  isERBUnlessNode(node) ||
27088
+ isERBRenderNode(node) ||
26567
27089
  isERBYieldNode(node) ||
26568
27090
  isERBInNode(node);
26569
27091
  }
@@ -26614,6 +27136,8 @@ const NODE_TYPE_GUARDS = new Map([
26614
27136
  [ERBEnsureNode, isERBEnsureNode],
26615
27137
  [ERBBeginNode, isERBBeginNode],
26616
27138
  [ERBUnlessNode, isERBUnlessNode],
27139
+ [RubyRenderLocalNode, isRubyRenderLocalNode],
27140
+ [ERBRenderNode, isERBRenderNode],
26617
27141
  [ERBYieldNode, isERBYieldNode],
26618
27142
  [ERBInNode, isERBInNode],
26619
27143
  ]);
@@ -26664,6 +27188,8 @@ const AST_TYPE_GUARDS = new Map([
26664
27188
  ["AST_ERB_ENSURE_NODE", isERBEnsureNode],
26665
27189
  ["AST_ERB_BEGIN_NODE", isERBBeginNode],
26666
27190
  ["AST_ERB_UNLESS_NODE", isERBUnlessNode],
27191
+ ["AST_RUBY_RENDER_LOCAL_NODE", isRubyRenderLocalNode],
27192
+ ["AST_ERB_RENDER_NODE", isERBRenderNode],
26667
27193
  ["AST_ERB_YIELD_NODE", isERBYieldNode],
26668
27194
  ["AST_ERB_IN_NODE", isERBInNode],
26669
27195
  ]);
@@ -26810,6 +27336,12 @@ function getStaticContentFromNodes(nodes) {
26810
27336
  }
26811
27337
  return literalNodes.map(node => node.content).join("");
26812
27338
  }
27339
+ /**
27340
+ * Checks if nodes contain any literal content (for static validation)
27341
+ */
27342
+ function hasStaticContent(nodes) {
27343
+ return nodes.some(isLiteralNode);
27344
+ }
26813
27345
  /**
26814
27346
  * Checks if nodes are effectively static (only literals and non-output ERB)
26815
27347
  * Non-output ERB like <% if %> doesn't affect static validation
@@ -26852,7 +27384,7 @@ function getCombinedStringFromNodes(nodes) {
26852
27384
  /**
26853
27385
  * Checks if an HTML attribute name node has dynamic content (contains ERB)
26854
27386
  */
26855
- function hasDynamicAttributeName(attributeNameNode) {
27387
+ function hasDynamicAttributeNameNode(attributeNameNode) {
26856
27388
  if (!attributeNameNode.children) {
26857
27389
  return false;
26858
27390
  }
@@ -26926,7 +27458,9 @@ function getAttributeName(attributeNode, lowercase = true) {
26926
27458
  return staticName ? staticName.toLowerCase() : null;
26927
27459
  }
26928
27460
  function hasStaticAttributeValue(nodeOrAttribute, attributeName) {
26929
- const attributeNode = nodeOrAttribute;
27461
+ const attributeNode = attributeName
27462
+ ? getAttribute(nodeOrAttribute, attributeName)
27463
+ : nodeOrAttribute;
26930
27464
  if (!attributeNode?.value?.children)
26931
27465
  return false;
26932
27466
  return attributeNode.value.children.every(isLiteralNode);
@@ -26985,12 +27519,11 @@ function hasAttribute(node, attributeName) {
26985
27519
  }
26986
27520
  /**
26987
27521
  * Checks if an attribute has a dynamic (ERB-containing) name.
26988
- * Accepts an HTMLAttributeNode (wraps the core HTMLAttributeNameNode-level check).
26989
27522
  */
26990
- function hasDynamicAttributeNameOnAttribute(attributeNode) {
27523
+ function hasDynamicAttributeName(attributeNode) {
26991
27524
  if (!isHTMLAttributeNameNode(attributeNode.name))
26992
27525
  return false;
26993
- return hasDynamicAttributeName(attributeNode.name);
27526
+ return hasDynamicAttributeNameNode(attributeNode.name);
26994
27527
  }
26995
27528
  /**
26996
27529
  * Gets the combined string representation of an attribute name (including ERB syntax).
@@ -27001,12 +27534,34 @@ function getCombinedAttributeNameString(attributeNode) {
27001
27534
  return "";
27002
27535
  return getCombinedAttributeName(attributeNode.name);
27003
27536
  }
27537
+ /**
27538
+ * Checks if an attribute value contains dynamic content (ERB)
27539
+ */
27540
+ function hasDynamicAttributeValue(attributeNode) {
27541
+ if (!attributeNode.value?.children)
27542
+ return false;
27543
+ return attributeNode.value.children.some(isERBContentNode);
27544
+ }
27004
27545
  /**
27005
27546
  * Gets the value nodes array from an attribute for dynamic inspection
27006
27547
  */
27007
27548
  function getAttributeValueNodes(attributeNode) {
27008
27549
  return attributeNode.value?.children || [];
27009
27550
  }
27551
+ /**
27552
+ * Checks if an attribute value contains any static content (for validation purposes)
27553
+ */
27554
+ function hasStaticAttributeValueContent(attributeNode) {
27555
+ return hasStaticContent(getAttributeValueNodes(attributeNode));
27556
+ }
27557
+ /**
27558
+ * Gets the static content of an attribute value (all literal parts combined).
27559
+ * Unlike getStaticAttributeValue, this extracts only the static portions from mixed content.
27560
+ * Returns the concatenated literal content, or null if no literal nodes exist.
27561
+ */
27562
+ function getStaticAttributeValueContent(attributeNode) {
27563
+ return getStaticContentFromNodes(getAttributeValueNodes(attributeNode));
27564
+ }
27010
27565
  /**
27011
27566
  * Gets the combined attribute value including both static text and ERB tag syntax.
27012
27567
  * For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>").
@@ -27050,6 +27605,14 @@ function getAttributeValueQuoteType(node) {
27050
27605
  }
27051
27606
  return "none";
27052
27607
  }
27608
+ /**
27609
+ * Checks if an attribute value is quoted
27610
+ */
27611
+ function isAttributeValueQuoted(attributeNode) {
27612
+ if (!isHTMLAttributeValueNode(attributeNode.value))
27613
+ return false;
27614
+ return !!attributeNode.value.quoted;
27615
+ }
27053
27616
  /**
27054
27617
  * Iterates over all attributes of an element or open tag node
27055
27618
  */
@@ -27343,6 +27906,19 @@ function createWhitespaceNode() {
27343
27906
  });
27344
27907
  }
27345
27908
 
27909
+ // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
27910
+ const HTML_BOOLEAN_ATTRIBUTES = new Set([
27911
+ "allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact",
27912
+ "controls", "declare", "default", "defer", "disabled", "formnovalidate",
27913
+ "hidden", "inert", "ismap", "itemscope", "loop", "multiple", "muted",
27914
+ "nomodule", "nohref", "noresize", "noshade", "novalidate", "nowrap",
27915
+ "open", "playsinline", "readonly", "required", "reversed", "scoped",
27916
+ "seamless", "selected", "sortable", "truespeed", "typemustmatch",
27917
+ ]);
27918
+ function isBooleanAttribute(attributeName) {
27919
+ return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
27920
+ }
27921
+
27346
27922
  /*
27347
27923
  * The following code is derived from the "js-levenshtein" repository,
27348
27924
  * Copyright (c) 2017 Gustaf Andersson (https://github.com/gustf/js-levenshtein)
@@ -27489,7 +28065,7 @@ function didyoumean(input, list, threshold) {
27489
28065
  }
27490
28066
 
27491
28067
  // NOTE: This file is generated by the templates/template.rb script and should not
27492
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/visitor.ts.erb
28068
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.2/templates/javascript/packages/core/src/visitor.ts.erb
27493
28069
  class Visitor {
27494
28070
  visit(node) {
27495
28071
  if (!node)
@@ -27668,6 +28244,15 @@ class Visitor {
27668
28244
  this.visitERBNode(node);
27669
28245
  this.visitChildNodes(node);
27670
28246
  }
28247
+ visitRubyRenderLocalNode(node) {
28248
+ this.visitNode(node);
28249
+ this.visitChildNodes(node);
28250
+ }
28251
+ visitERBRenderNode(node) {
28252
+ this.visitNode(node);
28253
+ this.visitERBNode(node);
28254
+ this.visitChildNodes(node);
28255
+ }
27671
28256
  visitERBYieldNode(node) {
27672
28257
  this.visitNode(node);
27673
28258
  this.visitERBNode(node);
@@ -28118,6 +28703,12 @@ class IdentityPrinter extends Printer {
28118
28703
  this.visit(node.end_node);
28119
28704
  }
28120
28705
  }
28706
+ visitERBRenderNode(node) {
28707
+ this.printERBNode(node);
28708
+ }
28709
+ visitRubyRenderLocalNode(_node) {
28710
+ // extracted metadata, nothing to print
28711
+ }
28121
28712
  visitERBYieldNode(node) {
28122
28713
  this.printERBNode(node);
28123
28714
  }
@@ -28668,7 +29259,7 @@ class ParserRule {
28668
29259
  get parserOptions() {
28669
29260
  return DEFAULT_LINTER_PARSER_OPTIONS;
28670
29261
  }
28671
- createOffense(message, location, autofixContext, severity) {
29262
+ createOffense(message, location, autofixContext, severity, tags) {
28672
29263
  return {
28673
29264
  rule: this.ruleName,
28674
29265
  code: this.ruleName,
@@ -28677,6 +29268,7 @@ class ParserRule {
28677
29268
  location,
28678
29269
  autofixContext,
28679
29270
  severity,
29271
+ tags,
28680
29272
  };
28681
29273
  }
28682
29274
  }
@@ -28696,7 +29288,7 @@ class LexerRule {
28696
29288
  get defaultConfig() {
28697
29289
  return DEFAULT_RULE_CONFIG;
28698
29290
  }
28699
- createOffense(message, location, autofixContext, severity) {
29291
+ createOffense(message, location, autofixContext, severity, tags) {
28700
29292
  return {
28701
29293
  rule: this.ruleName,
28702
29294
  code: this.ruleName,
@@ -28705,6 +29297,7 @@ class LexerRule {
28705
29297
  location,
28706
29298
  autofixContext,
28707
29299
  severity,
29300
+ tags,
28708
29301
  };
28709
29302
  }
28710
29303
  }
@@ -28730,7 +29323,7 @@ class SourceRule {
28730
29323
  get defaultConfig() {
28731
29324
  return DEFAULT_RULE_CONFIG;
28732
29325
  }
28733
- createOffense(message, location, autofixContext, severity) {
29326
+ createOffense(message, location, autofixContext, severity, tags) {
28734
29327
  return {
28735
29328
  rule: this.ruleName,
28736
29329
  code: this.ruleName,
@@ -28739,6 +29332,7 @@ class SourceRule {
28739
29332
  location,
28740
29333
  autofixContext,
28741
29334
  severity,
29335
+ tags,
28742
29336
  };
28743
29337
  }
28744
29338
  }
@@ -28764,7 +29358,7 @@ class BaseRuleVisitor extends Visitor {
28764
29358
  * Helper method to create an unbound lint offense (without severity).
28765
29359
  * The Linter will bind severity based on the rule's config.
28766
29360
  */
28767
- createOffense(message, location, autofixContext, severity) {
29361
+ createOffense(message, location, autofixContext, severity, tags) {
28768
29362
  return {
28769
29363
  rule: this.ruleName,
28770
29364
  code: this.ruleName,
@@ -28773,13 +29367,14 @@ class BaseRuleVisitor extends Visitor {
28773
29367
  location,
28774
29368
  autofixContext,
28775
29369
  severity,
29370
+ tags,
28776
29371
  };
28777
29372
  }
28778
29373
  /**
28779
29374
  * Helper method to add an offense to the offenses array
28780
29375
  */
28781
- addOffense(message, location, autofixContext, severity) {
28782
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29376
+ addOffense(message, location, autofixContext, severity, tags) {
29377
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
28783
29378
  }
28784
29379
  }
28785
29380
  /**
@@ -28866,13 +29461,6 @@ const HTML_VOID_ELEMENTS = new Set([
28866
29461
  "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
28867
29462
  "param", "source", "track", "wbr",
28868
29463
  ]);
28869
- const HTML_BOOLEAN_ATTRIBUTES = new Set([
28870
- "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
28871
- "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
28872
- "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
28873
- "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
28874
- "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
28875
- ]);
28876
29464
  const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
28877
29465
  /**
28878
29466
  * SVG elements that use camelCase naming
@@ -29027,12 +29615,6 @@ function isBlockElement(tagName) {
29027
29615
  function isVoidElement(tagName) {
29028
29616
  return HTML_VOID_ELEMENTS.has(tagName.toLowerCase());
29029
29617
  }
29030
- /**
29031
- * Checks if an attribute is a boolean attribute
29032
- */
29033
- function isBooleanAttribute(attributeName) {
29034
- return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
29035
- }
29036
29618
  /**
29037
29619
  * Attribute visitor that provides granular processing based on both
29038
29620
  * attribute name type (static/dynamic) and value type (static/dynamic)
@@ -29055,7 +29637,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
29055
29637
  forEachAttribute(node, (attributeNode) => {
29056
29638
  const staticAttributeName = getAttributeName(attributeNode);
29057
29639
  const originalAttributeName = getAttributeName(attributeNode, false) || "";
29058
- const isDynamicName = hasDynamicAttributeNameOnAttribute(attributeNode);
29640
+ const isDynamicName = hasDynamicAttributeName(attributeNode);
29059
29641
  const staticAttributeValue = getStaticAttributeValue(attributeNode);
29060
29642
  const valueNodes = getAttributeValueNodes(attributeNode);
29061
29643
  const hasOutputERB = hasERBOutput(valueNodes);
@@ -29132,7 +29714,7 @@ class BaseLexerRuleVisitor {
29132
29714
  * Helper method to create an unbound lint offense (without severity).
29133
29715
  * The Linter will bind severity based on the rule's config.
29134
29716
  */
29135
- createOffense(message, location, autofixContext, severity) {
29717
+ createOffense(message, location, autofixContext, severity, tags) {
29136
29718
  return {
29137
29719
  rule: this.ruleName,
29138
29720
  code: this.ruleName,
@@ -29141,13 +29723,14 @@ class BaseLexerRuleVisitor {
29141
29723
  location,
29142
29724
  autofixContext,
29143
29725
  severity,
29726
+ tags,
29144
29727
  };
29145
29728
  }
29146
29729
  /**
29147
29730
  * Helper method to add an offense to the offenses array
29148
29731
  */
29149
- addOffense(message, location, autofixContext, severity) {
29150
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29732
+ addOffense(message, location, autofixContext, severity, tags) {
29733
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
29151
29734
  }
29152
29735
  /**
29153
29736
  * Main entry point for lexer rule visitors
@@ -29188,7 +29771,7 @@ class BaseSourceRuleVisitor {
29188
29771
  * Helper method to create an unbound lint offense (without severity).
29189
29772
  * The Linter will bind severity based on the rule's config.
29190
29773
  */
29191
- createOffense(message, location, autofixContext, severity) {
29774
+ createOffense(message, location, autofixContext, severity, tags) {
29192
29775
  return {
29193
29776
  rule: this.ruleName,
29194
29777
  code: this.ruleName,
@@ -29197,13 +29780,14 @@ class BaseSourceRuleVisitor {
29197
29780
  location,
29198
29781
  autofixContext,
29199
29782
  severity,
29783
+ tags,
29200
29784
  };
29201
29785
  }
29202
29786
  /**
29203
29787
  * Helper method to add an offense to the offenses array
29204
29788
  */
29205
- addOffense(message, location, autofixContext, severity) {
29206
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
29789
+ addOffense(message, location, autofixContext, severity, tags) {
29790
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
29207
29791
  }
29208
29792
  /**
29209
29793
  * Main entry point for source rule visitors
@@ -29553,6 +30137,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
29553
30137
  }
29554
30138
  }
29555
30139
 
30140
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
30141
+ visitERBRenderNode(node) {
30142
+ if (!isERBOutputNode(node)) {
30143
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
30144
+ }
30145
+ this.visitChildNodes(node);
30146
+ }
30147
+ }
30148
+ class ActionViewNoSilentRenderRule extends ParserRule {
30149
+ static ruleName = "actionview-no-silent-render";
30150
+ get defaultConfig() {
30151
+ return {
30152
+ enabled: true,
30153
+ severity: "error"
30154
+ };
30155
+ }
30156
+ get parserOptions() {
30157
+ return {
30158
+ render_nodes: true,
30159
+ };
30160
+ }
30161
+ check(result, context) {
30162
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
30163
+ visitor.visit(result.value);
30164
+ return visitor.offenses;
30165
+ }
30166
+ }
30167
+
29556
30168
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
29557
30169
  visitERBContentNode(node) {
29558
30170
  const content = node.content?.value || "";
@@ -29617,7 +30229,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
29617
30229
  for (const child of node.children) {
29618
30230
  if (!this.isAllowedContent(child)) {
29619
30231
  const childCode = IdentityPrinter.print(child).trim();
29620
- 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);
30232
+ 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);
30233
+ offense.tags = ["unnecessary"];
30234
+ this.offenses.push(offense);
29621
30235
  }
29622
30236
  }
29623
30237
  }
@@ -29647,6 +30261,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
29647
30261
  }
29648
30262
  }
29649
30263
 
30264
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
30265
+ processedIfNodes = new Set();
30266
+ processedElseNodes = new Set();
30267
+ visitERBIfNode(node) {
30268
+ if (this.processedIfNodes.has(node)) {
30269
+ return;
30270
+ }
30271
+ this.markIfChainAsProcessed(node);
30272
+ this.markElseNodesInIfChain(node);
30273
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
30274
+ if (entireChainEmpty) {
30275
+ this.addEmptyBlockOffense(node, node.statements, "if");
30276
+ }
30277
+ else {
30278
+ this.checkIfChainParts(node);
30279
+ }
30280
+ this.visitChildNodes(node);
30281
+ }
30282
+ visitERBElseNode(node) {
30283
+ if (this.processedElseNodes.has(node)) {
30284
+ this.visitChildNodes(node);
30285
+ return;
30286
+ }
30287
+ this.addEmptyBlockOffense(node, node.statements, "else");
30288
+ this.visitChildNodes(node);
30289
+ }
30290
+ visitERBUnlessNode(node) {
30291
+ const unlessHasContent = this.statementsHaveContent(node.statements);
30292
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
30293
+ if (node.else_clause) {
30294
+ this.processedElseNodes.add(node.else_clause);
30295
+ }
30296
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
30297
+ if (entireBlockEmpty) {
30298
+ this.addEmptyBlockOffense(node, node.statements, "unless");
30299
+ }
30300
+ else {
30301
+ if (!unlessHasContent) {
30302
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
30303
+ }
30304
+ if (node.else_clause && !elseHasContent) {
30305
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
30306
+ }
30307
+ }
30308
+ this.visitChildNodes(node);
30309
+ }
30310
+ visitERBForNode(node) {
30311
+ this.addEmptyBlockOffense(node, node.statements, "for");
30312
+ this.visitChildNodes(node);
30313
+ }
30314
+ visitERBWhileNode(node) {
30315
+ this.addEmptyBlockOffense(node, node.statements, "while");
30316
+ this.visitChildNodes(node);
30317
+ }
30318
+ visitERBUntilNode(node) {
30319
+ this.addEmptyBlockOffense(node, node.statements, "until");
30320
+ this.visitChildNodes(node);
30321
+ }
30322
+ visitERBWhenNode(node) {
30323
+ if (!node.then_keyword) {
30324
+ this.addEmptyBlockOffense(node, node.statements, "when");
30325
+ }
30326
+ this.visitChildNodes(node);
30327
+ }
30328
+ visitERBInNode(node) {
30329
+ if (!node.then_keyword) {
30330
+ this.addEmptyBlockOffense(node, node.statements, "in");
30331
+ }
30332
+ this.visitChildNodes(node);
30333
+ }
30334
+ visitERBBeginNode(node) {
30335
+ this.addEmptyBlockOffense(node, node.statements, "begin");
30336
+ this.visitChildNodes(node);
30337
+ }
30338
+ visitERBRescueNode(node) {
30339
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
30340
+ this.visitChildNodes(node);
30341
+ }
30342
+ visitERBEnsureNode(node) {
30343
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
30344
+ this.visitChildNodes(node);
30345
+ }
30346
+ visitERBBlockNode(node) {
30347
+ this.addEmptyBlockOffense(node, node.body, "do");
30348
+ this.visitChildNodes(node);
30349
+ }
30350
+ addEmptyBlockOffense(node, statements, blockType) {
30351
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
30352
+ }
30353
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
30354
+ if (this.statementsHaveContent(statements)) {
30355
+ return;
30356
+ }
30357
+ const startLocation = node.location.start;
30358
+ const endLocation = subsequentNode
30359
+ ? subsequentNode.location.start
30360
+ : node.location.end;
30361
+ const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
30362
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
30363
+ offense.tags = ["unnecessary"];
30364
+ this.offenses.push(offense);
30365
+ }
30366
+ statementsHaveContent(statements) {
30367
+ return statements.some(statement => {
30368
+ if (isHTMLTextNode(statement)) {
30369
+ return statement.content.trim() !== "";
30370
+ }
30371
+ return true;
30372
+ });
30373
+ }
30374
+ markIfChainAsProcessed(node) {
30375
+ this.processedIfNodes.add(node);
30376
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30377
+ if (isERBIfNode(current)) {
30378
+ this.processedIfNodes.add(current);
30379
+ }
30380
+ });
30381
+ }
30382
+ markElseNodesInIfChain(node) {
30383
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30384
+ if (isERBElseNode(current)) {
30385
+ this.processedElseNodes.add(current);
30386
+ }
30387
+ });
30388
+ }
30389
+ traverseSubsequentNodes(startNode, callback) {
30390
+ let current = startNode;
30391
+ while (current) {
30392
+ if (isERBIfNode(current)) {
30393
+ callback(current);
30394
+ current = current.subsequent;
30395
+ }
30396
+ else if (isERBElseNode(current)) {
30397
+ callback(current);
30398
+ break;
30399
+ }
30400
+ else {
30401
+ break;
30402
+ }
30403
+ }
30404
+ }
30405
+ checkIfChainParts(node) {
30406
+ if (!this.statementsHaveContent(node.statements)) {
30407
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
30408
+ }
30409
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30410
+ if (this.statementsHaveContent(current.statements)) {
30411
+ return;
30412
+ }
30413
+ const blockType = isERBIfNode(current) ? "elsif" : "else";
30414
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
30415
+ if (nextSubsequent) {
30416
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
30417
+ }
30418
+ else {
30419
+ this.addEmptyBlockOffense(current, current.statements, blockType);
30420
+ }
30421
+ });
30422
+ }
30423
+ isEntireIfChainEmpty(node) {
30424
+ if (this.statementsHaveContent(node.statements)) {
30425
+ return false;
30426
+ }
30427
+ let hasContent = false;
30428
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
30429
+ if (this.statementsHaveContent(current.statements)) {
30430
+ hasContent = true;
30431
+ }
30432
+ });
30433
+ return !hasContent;
30434
+ }
30435
+ }
30436
+ class ERBNoEmptyControlFlowRule extends ParserRule {
30437
+ static ruleName = "erb-no-empty-control-flow";
30438
+ get defaultConfig() {
30439
+ return {
30440
+ enabled: true,
30441
+ severity: "hint"
30442
+ };
30443
+ }
30444
+ check(result, context) {
30445
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
30446
+ visitor.visit(result.value);
30447
+ return visitor.offenses;
30448
+ }
30449
+ }
30450
+
29650
30451
  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; }
29651
30452
  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; }
29652
30453
  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; }
@@ -29805,6 +30606,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
29805
30606
  function getSignificantNodes(statements) {
29806
30607
  return statements.filter(node => !isPureWhitespaceNode(node));
29807
30608
  }
30609
+ function trimWhitespaceNodes(nodes) {
30610
+ let start = 0;
30611
+ let end = nodes.length;
30612
+ while (start < end && isPureWhitespaceNode(nodes[start]))
30613
+ start++;
30614
+ while (end > start && isPureWhitespaceNode(nodes[end - 1]))
30615
+ end--;
30616
+ return nodes.slice(start, end);
30617
+ }
29808
30618
  function allEquivalentElements(nodes) {
29809
30619
  if (nodes.length < 2)
29810
30620
  return false;
@@ -29922,9 +30732,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
29922
30732
  if (isERBIfNode(node)) {
29923
30733
  this.markSubsequentIfNodesAsProcessed(node);
29924
30734
  }
30735
+ if (this.allBranchesIdentical(branches)) {
30736
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
30737
+ return;
30738
+ }
29925
30739
  const state = { isFirstOffense: true };
29926
30740
  this.checkBranches(branches, node, state);
29927
30741
  }
30742
+ allBranchesIdentical(branches) {
30743
+ if (branches.length < 2)
30744
+ return false;
30745
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
30746
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
30747
+ }
29928
30748
  markSubsequentIfNodesAsProcessed(node) {
29929
30749
  let current = node.subsequent;
29930
30750
  while (current) {
@@ -29958,11 +30778,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
29958
30778
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
29959
30779
  for (const element of elements) {
29960
30780
  const printed = IdentityPrinter.print(element.open_tag);
29961
- const autofixContext = state.isFirstOffense
29962
- ? { node: conditionalNode }
29963
- : undefined;
29964
- 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);
29965
- state.isFirstOffense = false;
30781
+ if (bodiesMatch) {
30782
+ const autofixContext = state.isFirstOffense
30783
+ ? { node: conditionalNode }
30784
+ : undefined;
30785
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
30786
+ state.isFirstOffense = false;
30787
+ }
30788
+ else {
30789
+ const autofixContext = state.isFirstOffense
30790
+ ? { node: conditionalNode }
30791
+ : undefined;
30792
+ const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
30793
+ ? element.open_tag.tag_name.location
30794
+ : element?.open_tag?.location || element.location;
30795
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
30796
+ state.isFirstOffense = false;
30797
+ }
29966
30798
  }
29967
30799
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
29968
30800
  this.checkBranches(bodies, conditionalNode, state);
@@ -29991,6 +30823,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
29991
30823
  const branches = collectBranches(conditionalNode);
29992
30824
  if (!branches)
29993
30825
  return null;
30826
+ if (offense.autofixContext.allIdentical) {
30827
+ const parentInfo = findParentArray(result.value, conditionalNode);
30828
+ if (!parentInfo)
30829
+ return null;
30830
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
30831
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
30832
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
30833
+ return result;
30834
+ }
29994
30835
  const significantBranches = branches.map(getSignificantNodes);
29995
30836
  if (significantBranches.some(branch => branch.length === 0))
29996
30837
  return null;
@@ -30004,23 +30845,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
30004
30845
  return null;
30005
30846
  let { array: parentArray, index: conditionalIndex } = parentInfo;
30006
30847
  let hasWrapped = false;
30848
+ let didMutate = false;
30849
+ let failedToHoistPrefix = false;
30850
+ let hoistedBefore = false;
30007
30851
  const hoistElement = (elements, position) => {
30852
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
30008
30853
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
30009
30854
  if (bodiesMatch) {
30855
+ if (actualPosition === "after") {
30856
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
30857
+ if (currentLengths.some(l => l !== currentLengths[0]))
30858
+ return;
30859
+ }
30860
+ if (actualPosition === "after" && position === "before") {
30861
+ const isAtEnd = branches.every((branch, index) => {
30862
+ const nodes = getSignificantNodes(branch);
30863
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
30864
+ });
30865
+ if (!isAtEnd)
30866
+ return;
30867
+ }
30010
30868
  for (let i = 0; i < branches.length; i++) {
30011
30869
  removeNodeFromArray(branches[i], elements[i]);
30012
30870
  }
30013
- if (position === "before") {
30014
- parentArray.splice(conditionalIndex, 0, elements[0]);
30015
- conditionalIndex++;
30871
+ if (actualPosition === "before") {
30872
+ parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
30873
+ conditionalIndex += 2;
30874
+ hoistedBefore = true;
30016
30875
  }
30017
30876
  else {
30018
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
30877
+ parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
30019
30878
  }
30879
+ didMutate = true;
30020
30880
  }
30021
30881
  else {
30022
30882
  if (hasWrapped)
30023
30883
  return;
30884
+ const canWrap = branches.every((branch, index) => {
30885
+ const remaining = getSignificantNodes(branch);
30886
+ return remaining.length === 1 && remaining[0] === elements[index];
30887
+ });
30888
+ if (!canWrap) {
30889
+ if (position === "before")
30890
+ failedToHoistPrefix = true;
30891
+ return;
30892
+ }
30024
30893
  for (let i = 0; i < branches.length; i++) {
30025
30894
  replaceNodeWithBody(branches[i], elements[i]);
30026
30895
  }
@@ -30029,6 +30898,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
30029
30898
  parentArray = wrapper.body;
30030
30899
  conditionalIndex = 1;
30031
30900
  hasWrapped = true;
30901
+ didMutate = true;
30032
30902
  }
30033
30903
  };
30034
30904
  for (let index = 0; index < prefixCount; index++) {
@@ -30039,7 +30909,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
30039
30909
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
30040
30910
  hoistElement(elements, "after");
30041
30911
  }
30042
- return result;
30912
+ if (!hasWrapped && hoistedBefore) {
30913
+ const remaining = branches.map(branch => getSignificantNodes(branch));
30914
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
30915
+ const elements = remaining.map(b => b[0]);
30916
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
30917
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
30918
+ for (let i = 0; i < branches.length; i++) {
30919
+ replaceNodeWithBody(branches[i], elements[i]);
30920
+ }
30921
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
30922
+ parentArray[conditionalIndex] = wrapper;
30923
+ didMutate = true;
30924
+ }
30925
+ }
30926
+ }
30927
+ return didMutate ? result : null;
30043
30928
  }
30044
30929
  }
30045
30930
 
@@ -30565,6 +31450,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
30565
31450
  }
30566
31451
  }
30567
31452
 
31453
+ function isAssignmentNode(prismNode) {
31454
+ const type = prismNode?.constructor?.name;
31455
+ if (!type)
31456
+ return false;
31457
+ return type.endsWith("WriteNode");
31458
+ }
31459
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
31460
+ visitERBContentNode(node) {
31461
+ if (isERBOutputNode(node))
31462
+ return;
31463
+ const prismNode = node.prismNode;
31464
+ if (!prismNode)
31465
+ return;
31466
+ if (isAssignmentNode(prismNode))
31467
+ return;
31468
+ const content = node.content?.value?.trim();
31469
+ if (!content)
31470
+ return;
31471
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
31472
+ }
31473
+ }
31474
+ class ERBNoSilentStatementRule extends ParserRule {
31475
+ static ruleName = "erb-no-silent-statement";
31476
+ get defaultConfig() {
31477
+ return {
31478
+ enabled: false,
31479
+ severity: "warning"
31480
+ };
31481
+ }
31482
+ get parserOptions() {
31483
+ return {
31484
+ prism_nodes: true,
31485
+ };
31486
+ }
31487
+ check(result, context) {
31488
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
31489
+ visitor.visit(result.value);
31490
+ return visitor.offenses;
31491
+ }
31492
+ }
31493
+
30568
31494
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
30569
31495
  visitHTMLAttributeNameNode(node) {
30570
31496
  const erbNodes = filterERBContentNodes(node.children);
@@ -30827,7 +31753,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
30827
31753
  }
30828
31754
 
30829
31755
  const JS_ATTRIBUTE_PATTERN = /^on/i;
30830
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
31756
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
30831
31757
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
30832
31758
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
30833
31759
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -30838,7 +31764,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
30838
31764
  if (!isERBOutputNode(node))
30839
31765
  continue;
30840
31766
  const content = node.content?.value?.trim() || "";
30841
- if (SAFE_PATTERN$1.test(content))
31767
+ if (SAFE_PATTERN.test(content))
30842
31768
  continue;
30843
31769
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
30844
31770
  }
@@ -30919,7 +31845,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
30919
31845
  }
30920
31846
  }
30921
31847
 
30922
- const SAFE_PATTERN = /\.to_json\b/;
31848
+ const SAFE_METHOD_NAMES = new Set([
31849
+ "to_json",
31850
+ "json_escape",
31851
+ ]);
31852
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
31853
+ "j",
31854
+ "escape_javascript",
31855
+ ]);
31856
+ class SafeCallDetector extends Visitor$1 {
31857
+ hasSafeCall = false;
31858
+ hasEscapeJavascriptCall = false;
31859
+ visitCallNode(node) {
31860
+ if (SAFE_METHOD_NAMES.has(node.name)) {
31861
+ this.hasSafeCall = true;
31862
+ }
31863
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
31864
+ this.hasEscapeJavascriptCall = true;
31865
+ }
31866
+ this.visitChildNodes(node);
31867
+ }
31868
+ }
30923
31869
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
30924
31870
  visitHTMLElementNode(node) {
30925
31871
  if (!isHTMLOpenTagNode(node.open_tag)) {
@@ -30948,9 +31894,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
30948
31894
  continue;
30949
31895
  if (!isERBOutputNode(child))
30950
31896
  continue;
30951
- const content = child.content?.value?.trim() || "";
30952
- if (SAFE_PATTERN.test(content))
31897
+ const erbContent = child;
31898
+ const prismNode = erbContent.prismNode;
31899
+ const detector = new SafeCallDetector();
31900
+ if (prismNode)
31901
+ detector.visit(prismNode);
31902
+ if (detector.hasSafeCall)
31903
+ continue;
31904
+ if (detector.hasEscapeJavascriptCall) {
31905
+ 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);
30953
31906
  continue;
31907
+ }
30954
31908
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
30955
31909
  }
30956
31910
  }
@@ -30963,6 +31917,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
30963
31917
  severity: "error"
30964
31918
  };
30965
31919
  }
31920
+ get parserOptions() {
31921
+ return {
31922
+ prism_nodes: true,
31923
+ };
31924
+ }
30966
31925
  check(result, context) {
30967
31926
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
30968
31927
  visitor.visit(result.value);
@@ -31970,7 +32929,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
31970
32929
  }
31971
32930
  }
31972
32931
 
31973
- const ALLOWED_TYPES = ["text/javascript"];
32932
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
31974
32933
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
31975
32934
  visitHTMLOpenTagNode(node) {
31976
32935
  if (getTagLocalName(node) === "script") {
@@ -32508,6 +33467,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
32508
33467
  }
32509
33468
  }
32510
33469
 
33470
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
33471
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
33472
+ this.checkAttribute(originalAttributeName, attributeNode);
33473
+ }
33474
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
33475
+ this.checkAttribute(originalAttributeName, attributeNode);
33476
+ }
33477
+ checkAttribute(attributeName, attributeNode) {
33478
+ if (!isBooleanAttribute(attributeName))
33479
+ return;
33480
+ if (!hasAttributeValue(attributeNode))
33481
+ return;
33482
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
33483
+ node: attributeNode
33484
+ });
33485
+ }
33486
+ }
33487
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
33488
+ static autocorrectable = true;
33489
+ static ruleName = "html-boolean-attributes-no-value";
33490
+ get defaultConfig() {
33491
+ return {
33492
+ enabled: true,
33493
+ severity: "error"
33494
+ };
33495
+ }
33496
+ check(result, context) {
33497
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
33498
+ visitor.visit(result.value);
33499
+ return visitor.offenses;
33500
+ }
33501
+ autofix(offense, result, _context) {
33502
+ if (!offense.autofixContext)
33503
+ return null;
33504
+ const { node } = offense.autofixContext;
33505
+ node.equals = null;
33506
+ node.value = null;
33507
+ return result;
33508
+ }
33509
+ }
33510
+
32511
33511
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
32512
33512
  visitHTMLElementNode(node) {
32513
33513
  this.checkDetailsElement(node);
@@ -32557,47 +33557,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
32557
33557
  }
32558
33558
  }
32559
33559
 
32560
- class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
32561
- checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
32562
- this.checkAttribute(originalAttributeName, attributeNode);
32563
- }
32564
- checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
32565
- this.checkAttribute(originalAttributeName, attributeNode);
32566
- }
32567
- checkAttribute(attributeName, attributeNode) {
32568
- if (!isBooleanAttribute(attributeName))
32569
- return;
32570
- if (!hasAttributeValue(attributeNode))
32571
- return;
32572
- this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
32573
- node: attributeNode
32574
- });
32575
- }
32576
- }
32577
- class HTMLBooleanAttributesNoValueRule extends ParserRule {
32578
- static autocorrectable = true;
32579
- static ruleName = "html-boolean-attributes-no-value";
32580
- get defaultConfig() {
32581
- return {
32582
- enabled: true,
32583
- severity: "error"
32584
- };
32585
- }
32586
- check(result, context) {
32587
- const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
32588
- visitor.visit(result.value);
32589
- return visitor.offenses;
32590
- }
32591
- autofix(offense, result, _context) {
32592
- if (!offense.autofixContext)
32593
- return null;
32594
- const { node } = offense.autofixContext;
32595
- node.equals = null;
32596
- node.value = null;
32597
- return result;
32598
- }
32599
- }
32600
-
32601
33560
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
32602
33561
  elementStack = [];
32603
33562
  visitHTMLElementNode(node) {
@@ -34280,8 +35239,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
34280
35239
 
34281
35240
  const rules = [
34282
35241
  ActionViewNoSilentHelperRule,
35242
+ ActionViewNoSilentRenderRule,
34283
35243
  ERBCommentSyntax,
34284
35244
  ERBNoCaseNodeChildrenRule,
35245
+ ERBNoEmptyControlFlowRule,
34285
35246
  ERBNoConditionalHTMLElementRule,
34286
35247
  ERBNoConditionalOpenTagRule,
34287
35248
  ERBNoDuplicateBranchElementsRule,
@@ -34296,6 +35257,7 @@ const rules = [
34296
35257
  ERBNoOutputInAttributeNameRule,
34297
35258
  ERBNoOutputInAttributePositionRule,
34298
35259
  ERBNoRawOutputInAttributeValueRule,
35260
+ ERBNoSilentStatementRule,
34299
35261
  ERBNoSilentTagInAttributeNameRule,
34300
35262
  ERBNoStatementInScriptRule,
34301
35263
  ERBNoThenInControlFlowRule,
@@ -34327,8 +35289,8 @@ const rules = [
34327
35289
  HTMLAttributeValuesRequireQuotesRule,
34328
35290
  HTMLAvoidBothDisabledAndAriaDisabledRule,
34329
35291
  HTMLBodyOnlyElementsRule,
34330
- HTMLDetailsHasSummaryRule,
34331
35292
  HTMLBooleanAttributesNoValueRule,
35293
+ HTMLDetailsHasSummaryRule,
34332
35294
  HTMLHeadOnlyElementsRule,
34333
35295
  HTMLIframeHasTitleRule,
34334
35296
  HTMLImgRequireAltRule,
@@ -35905,6 +36867,8 @@ async function loadCustomRules(options) {
35905
36867
 
35906
36868
  exports.ABSTRACT_ARIA_ROLES = ABSTRACT_ARIA_ROLES;
35907
36869
  exports.ARIA_ATTRIBUTES = ARIA_ATTRIBUTES;
36870
+ exports.ActionViewNoSilentHelperRule = ActionViewNoSilentHelperRule;
36871
+ exports.ActionViewNoSilentRenderRule = ActionViewNoSilentRenderRule;
35908
36872
  exports.AttributeVisitorMixin = AttributeVisitorMixin;
35909
36873
  exports.BaseLexerRuleVisitor = BaseLexerRuleVisitor;
35910
36874
  exports.BaseRuleVisitor = BaseRuleVisitor;
@@ -35919,6 +36883,7 @@ exports.ERBCommentSyntax = ERBCommentSyntax;
35919
36883
  exports.ERBNoCaseNodeChildrenRule = ERBNoCaseNodeChildrenRule;
35920
36884
  exports.ERBNoConditionalOpenTagRule = ERBNoConditionalOpenTagRule;
35921
36885
  exports.ERBNoDuplicateBranchElementsRule = ERBNoDuplicateBranchElementsRule;
36886
+ exports.ERBNoEmptyControlFlowRule = ERBNoEmptyControlFlowRule;
35922
36887
  exports.ERBNoEmptyTagsRule = ERBNoEmptyTagsRule;
35923
36888
  exports.ERBNoExtraNewLineRule = ERBNoExtraNewLineRule;
35924
36889
  exports.ERBNoExtraWhitespaceRule = ERBNoExtraWhitespaceRule;
@@ -35929,6 +36894,7 @@ exports.ERBNoOutputControlFlowRule = ERBNoOutputControlFlowRule;
35929
36894
  exports.ERBNoOutputInAttributeNameRule = ERBNoOutputInAttributeNameRule;
35930
36895
  exports.ERBNoOutputInAttributePositionRule = ERBNoOutputInAttributePositionRule;
35931
36896
  exports.ERBNoRawOutputInAttributeValueRule = ERBNoRawOutputInAttributeValueRule;
36897
+ exports.ERBNoSilentStatementRule = ERBNoSilentStatementRule;
35932
36898
  exports.ERBNoSilentTagInAttributeNameRule = ERBNoSilentTagInAttributeNameRule;
35933
36899
  exports.ERBNoStatementInScriptRule = ERBNoStatementInScriptRule;
35934
36900
  exports.ERBNoThenInControlFlowRule = ERBNoThenInControlFlowRule;
@@ -36003,11 +36969,29 @@ exports.SVG_LOWERCASE_TO_CAMELCASE = SVG_LOWERCASE_TO_CAMELCASE;
36003
36969
  exports.SourceRule = SourceRule;
36004
36970
  exports.VALID_ARIA_ROLES = VALID_ARIA_ROLES;
36005
36971
  exports.createEndOfFileLocation = createEndOfFileLocation;
36972
+ exports.findAttributeByName = findAttributeByName;
36006
36973
  exports.findNodeAtPosition = findNodeAtPosition;
36007
36974
  exports.findNodeByLocation = findNodeByLocation;
36008
36975
  exports.findParent = findParent;
36976
+ exports.getAttribute = getAttribute;
36977
+ exports.getAttributeName = getAttributeName;
36978
+ exports.getAttributeValue = getAttributeValue;
36979
+ exports.getAttributeValueNodes = getAttributeValueNodes;
36980
+ exports.getAttributeValueQuoteType = getAttributeValueQuoteType;
36981
+ exports.getAttributes = getAttributes;
36009
36982
  exports.getBasename = getBasename;
36983
+ exports.getCombinedAttributeNameString = getCombinedAttributeNameString;
36984
+ exports.getStaticAttributeValue = getStaticAttributeValue;
36985
+ exports.getStaticAttributeValueContent = getStaticAttributeValueContent;
36986
+ exports.getTagName = getTagName;
36987
+ exports.hasAttribute = hasAttribute;
36988
+ exports.hasAttributeValue = hasAttributeValue;
36010
36989
  exports.hasBalancedParentheses = hasBalancedParentheses;
36990
+ exports.hasDynamicAttributeName = hasDynamicAttributeName;
36991
+ exports.hasDynamicAttributeValue = hasDynamicAttributeValue;
36992
+ exports.hasStaticAttributeValue = hasStaticAttributeValue;
36993
+ exports.hasStaticAttributeValueContent = hasStaticAttributeValueContent;
36994
+ exports.isAttributeValueQuoted = isAttributeValueQuoted;
36011
36995
  exports.isBlockElement = isBlockElement;
36012
36996
  exports.isBodyOnlyTag = isBodyOnlyTag;
36013
36997
  exports.isBodyTag = isBodyTag;