Repository: ruby/prism (c)

Issue:
`IndexTargetNode` doesn't have `ATTRIBUTE_WRITE`
Not sure if it's expected behaviour, but it will be nice to have the flag set:

```
bin/parse -e 'a[b], = []'
@ ProgramNode (location: (1,0)-(1,10))
├── locals: []
└── statements:
    @ StatementsNode (location: (1,0)-(1,10))
    └── body: (length: 1)
        └── @ MultiWriteNode (location: (1,0)-(1,10))
            ├── lefts: (length: 1)
            │   └── @ IndexTargetNode (location: (1,0)-(1,4))
            │       ├── flags: ∅
            │       ├── receiver:
            │       │   @ CallNode (location: (1,0)-(1,1))
            │       │   ├── flags: variable_call
            │       │   ├── receiver: ∅
            │       │   ├── call_operator_loc: ∅
            │       │   ├── name: :a
            │       │   ├── message_loc: (1,0)-(1,1) = "a"
            │       │   ├── opening_loc: ∅
            │       │   ├── arguments: ∅
            │       │   ├── closing_loc: ∅
            │       │   └── block: ∅
            │       ├── opening_loc: (1,1)-(1,2) = "["
            │       ├── arguments:
            │       │   @ ArgumentsNode (location: (1,2)-(1,3))
            │       │   ├── flags: ∅
            │       │   └── arguments: (length: 1)
            │       │       └── @ CallNode (location: (1,2)-(1,3))
            │       │           ├── flags: variable_call
            │       │           ├── receiver: ∅
            │       │           ├── call_operator_loc: ∅
            │       │           ├── name: :b
            │       │           ├── message_loc: (1,2)-(1,3) = "b"
            │       │           ├── opening_loc: ∅
            │       │           ├── arguments: ∅
            │       │           ├── closing_loc: ∅
            │       │           └── block: ∅
            │       ├── closing_loc: (1,3)-(1,4) = "]"
            │       └── block: ∅
            ├── rest:
            │   @ ImplicitRestNode (location: (1,4)-(1,5))
            ├── rights: (length: 0)
            ├── lparen_loc: ∅
            ├── rparen_loc: ∅
            ├── operator_loc: (1,6)-(1,7) = "="
            └── value:
                @ ArrayNode (location: (1,8)-(1,10))
                ├── flags: ∅
                ├── elements: (length: 0)
                ├── opening_loc: (1,8)-(1,9) = "["
                └── closing_loc: (1,9)-(1,10) = "]"
```

Pull request description:
IndexTargetNode should always have ATTRIBUTE_WRITE
Fixes #2102

Because this is a user-facing change, we also need to deal with the fact that CRuby 3.3.0 was just released.

In order to support workflows that want to parse exactly as CRuby parses in a specific version, this PR introduces a new option to the options struct that is "version". This allows you to specify that you want "3.3.0" parsing.

I'm not sure if this is the correct solution. Another solution is to just fork and keep around the old branch for security patches. Or we could keep around a copy of the source files within this repository as another directory and only update when necessary. There are a lot of potential solutions here.

Because this change is so small and the check for it is so minimal, I've decided to go with this enum. If this ends up entirely cluttering the codebase with version checks, we'll come up with another solution. But for now this works, so we're going to go in this direction for a bit until we determine it's no longer working.

Patch:
diff --git a/.gitignore b/.gitignore
index 8798e6ed..ceaf1f86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,7 @@ out.svg
 /javascript/src/nodes.js
 /javascript/src/visitor.js
 /javascript/src/prism.wasm
-/javascript/src/*.d.ts
+/javascript/types/
 /java/org/prism/AbstractNodeVisitor.java
 /java/org/prism/Loader.java
 /java/org/prism/Nodes.java
diff --git a/ext/prism/extension.c b/ext/prism/extension.c
index 6cf007b4..6dc19936 100644
--- a/ext/prism/extension.c
+++ b/ext/prism/extension.c
@@ -22,6 +22,7 @@ ID rb_option_id_encoding;
 ID rb_option_id_line;
 ID rb_option_id_frozen_string_literal;
 ID rb_option_id_verbose;
+ID rb_option_id_version;
 ID rb_option_id_scopes;
 
 /******************************************************************************/
@@ -131,6 +132,14 @@ build_options_i(VALUE key, VALUE value, VALUE argument) {
         if (!NIL_P(value)) pm_options_frozen_string_literal_set(options, value == Qtrue);
     } else if (key_id == rb_option_id_verbose) {
         pm_options_suppress_warnings_set(options, value != Qtrue);
+    } else if (key_id == rb_option_id_version) {
+        if (!NIL_P(value)) {
+            const char *version = check_string(value);
+
+            if (!pm_options_version_set(options, version, RSTRING_LEN(value))) {
+                rb_raise(rb_eArgError, "invalid version: %"PRIsVALUE, value);
+            }
+        }
     } else if (key_id == rb_option_id_scopes) {
         if (!NIL_P(value)) build_options_scopes(options, value);
     } else {
@@ -1013,6 +1022,7 @@ Init_prism(void) {
     rb_option_id_line = rb_intern_const("line");
     rb_option_id_frozen_string_literal = rb_intern_const("frozen_string_literal");
     rb_option_id_verbose = rb_intern_const("verbose");
+    rb_option_id_version = rb_intern_const("version");
     rb_option_id_scopes = rb_intern_const("scopes");
 
     /**
diff --git a/include/prism.h b/include/prism.h
index f4a24827..869d608c 100644
--- a/include/prism.h
+++ b/include/prism.h
@@ -260,7 +260,7 @@ PRISM_EXPORTED_FUNCTION const char * pm_token_type_to_str(pm_token_type_t token_
  *     pm_buffer_t buffer = { 0 };
  *
  *     pm_prettyprint(&buffer, &parser, root);
- *     printf("%*.s\n", (int) buffer.length, buffer.value);
+ *     printf("*.s%\n", (int) buffer.length, buffer.value);
  *
  *     pm_buffer_free(&buffer);
  *     pm_node_destroy(&parser, root);
diff --git a/include/prism/options.h b/include/prism/options.h
index 8608838d..130d635b 100644
--- a/include/prism/options.h
+++ b/include/prism/options.h
@@ -24,6 +24,19 @@ typedef struct pm_options_scope {
     pm_string_t *locals;
 } pm_options_scope_t;
 
+/**
+ * The version of prism that we should be parsing with. This is used to allow
+ * consumers to specify which behavior they want in case they need to parse
+ * exactly as a specific version of CRuby.
+ */
+typedef enum {
+    /** The current version of prism. */
+    PM_OPTIONS_VERSION_LATEST = 0,
+
+    /** The vendored version of prism in CRuby 3.3.0. */
+    PM_OPTIONS_VERSION_CRUBY_3_3_0 = 1
+} pm_options_version_t;
+
 /**
  * The options that can be passed to the parser.
  */
@@ -55,6 +68,13 @@ typedef struct {
      */
     pm_options_scope_t *scopes;
 
+    /**
+     * The version of prism that we should be parsing with. This is used to
+     * allow consumers to specify which behavior they want in case they need to
+     * parse exactly as a specific version of CRuby.
+     */
+    pm_options_version_t version;
+
     /** Whether or not the frozen string literal option has been set. */
     bool frozen_string_literal;
 
@@ -106,6 +126,18 @@ PRISM_EXPORTED_FUNCTION void pm_options_frozen_string_literal_set(pm_options_t *
  */
 PRISM_EXPORTED_FUNCTION void pm_options_suppress_warnings_set(pm_options_t *options, bool suppress_warnings);
 
+/**
+ * Set the version option on the given options struct by parsing the given
+ * string. If the string contains an invalid option, this returns false.
+ * Otherwise, it returns true.
+ *
+ * @param options The options struct to set the version on.
+ * @param version The version to set.
+ * @param length The length of the version string.
+ * @return Whether or not the version was parsed successfully.
+ */
+PRISM_EXPORTED_FUNCTION bool pm_options_version_set(pm_options_t *options, const char *version, size_t length);
+
 /**
  * Allocate and zero out the scopes array on the given options struct.
  *
@@ -167,9 +199,17 @@ PRISM_EXPORTED_FUNCTION void pm_options_free(pm_options_t *options);
  * | ...     | the encoding bytes         |
  * | `1`     | frozen string literal      |
  * | `1`     | suppress warnings          |
+ * | `1`     | the version                |
  * | `4`     | the number of scopes       |
  * | ...     | the scopes                 |
  *
+ * The version field is an enum, so it should be one of the following values:
+ *
+ * | value | version                   |
+ * | ----- | ------------------------- |
+ * | `0`   | use the latest version of prism |
+ * | `1`   | use the version of prism that is vendored in CRuby 3.3.0 |
+ *
  * Each scope is layed out as follows:
  *
  * | # bytes | field                      |
diff --git a/include/prism/parser.h b/include/prism/parser.h
index 2c58131b..9f38dc58 100644
--- a/include/prism/parser.h
+++ b/include/prism/parser.h
@@ -9,6 +9,7 @@
 #include "prism/ast.h"
 #include "prism/defines.h"
 #include "prism/encoding.h"
+#include "prism/options.h"
 #include "prism/util/pm_constant_pool.h"
 #include "prism/util/pm_list.h"
 #include "prism/util/pm_newline_list.h"
@@ -662,6 +663,12 @@ struct pm_parser {
      */
     const pm_encoding_t *explicit_encoding;
 
+    /** The current parameter name id on parsing its default value. */
+    pm_constant_id_t current_param_name;
+
+    /** The version of prism that we should use to parse. */
+    pm_options_version_t version;
+
     /** Whether or not we're at the beginning of a command. */
     bool command_start;
 
@@ -684,9 +691,6 @@ struct pm_parser {
     /** This flag indicates that we are currently parsing a keyword argument. */
     bool in_keyword_arg;
 
-    /** The current parameter name id on parsing its default value. */
-    pm_constant_id_t current_param_name;
-
     /**
      * Whether or not the parser has seen a token that has semantic meaning
      * (i.e., a token that is not a comment or whitespace).
diff --git a/javascript/package.json b/javascript/package.json
index eaef1519..ae0a4148 100644
--- a/javascript/package.json
+++ b/javascript/package.json
@@ -8,7 +8,7 @@
   "scripts": {
     "prepublishOnly": "npm run type",
     "test": "node test.js",
-    "type": "tsc --allowJs -d --emitDeclarationOnly --outDir src src/index.js"
+    "type": "tsc --allowJs -d --emitDeclarationOnly --outDir types src/index.js"
   },
   "author": "Shopify <ruby@shopify.com>",
   "license": "MIT"
diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb
index 8324f722..4c42e506 100644
--- a/lib/prism/ffi.rb
+++ b/lib/prism/ffi.rb
@@ -314,6 +314,9 @@ module Prism
       template << "C"
       values << (options.fetch(:verbose, true) ? 0 : 1)
 
+      template << "C"
+      values << { nil => 0, "3.3.0" => 1, "latest" => 0 }.fetch(options[:version])
+
       template << "L"
       if (scopes = options[:scopes])
         values << scopes.length
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index a84494c9..c5ef5364 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -365,9 +365,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
 name = "unsafe-libyaml"
-version = "0.2.10"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
 
 [[package]]
 name = "which"
diff --git a/src/options.c b/src/options.c
index 85d04d62..0dcae0d1 100644
--- a/src/options.c
+++ b/src/options.c
@@ -40,6 +40,33 @@ pm_options_suppress_warnings_set(pm_options_t *options, bool suppress_warnings)
     options->suppress_warnings = suppress_warnings;
 }
 
+/**
+ * Set the version option on the given options struct by parsing the given
+ * string. If the string contains an invalid option, this returns false.
+ * Otherwise, it returns true.
+ */
+PRISM_EXPORTED_FUNCTION bool
+pm_options_version_set(pm_options_t *options, const char *version, size_t length) {
+    if (version == NULL && length == 0) {
+        options->version = PM_OPTIONS_VERSION_LATEST;
+        return true;
+    }
+
+    if (length == 5) {
+        if (strncmp(version, "3.3.0", 5) == 0) {
+            options->version = PM_OPTIONS_VERSION_CRUBY_3_3_0;
+            return true;
+        }
+
+        if (strncmp(version, "latest", 6) == 0) {
+            options->version = PM_OPTIONS_VERSION_LATEST;
+            return true;
+        }
+    }
+
+    return false;
+}
+
 /**
  * Allocate and zero out the scopes array on the given options struct.
  */
@@ -163,6 +190,7 @@ pm_options_read(pm_options_t *options, const char *data) {
 
     options->frozen_string_literal = *data++;
     options->suppress_warnings = *data++;
+    options->version = (pm_options_version_t) *data++;
 
     uint32_t scopes_count = pm_options_read_u32(data);
     data += 4;
diff --git a/src/prism.c b/src/prism.c
index ec319f2c..03df6984 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -2172,11 +2172,16 @@ pm_call_target_node_create(pm_parser_t *parser, pm_call_node_t *target) {
 static pm_index_target_node_t *
 pm_index_target_node_create(pm_parser_t *parser, pm_call_node_t *target) {
     pm_index_target_node_t *node = PM_ALLOC_NODE(parser, pm_index_target_node_t);
+    pm_node_flags_t flags = target->base.flags;
+
+    if (parser->version != PM_OPTIONS_VERSION_CRUBY_3_3_0) {
+        flags |= PM_CALL_NODE_FLAGS_ATTRIBUTE_WRITE;
+    }
 
     *node = (pm_index_target_node_t) {
         {
             .type = PM_INDEX_TARGET_NODE,
-            .flags = target->base.flags,
+            .flags = flags,
             .location = target->base.location
         },
         .receiver = target->receiver,
@@ -17320,6 +17325,9 @@ pm_parser_init(pm_parser_t *parser, const uint8_t *source, size_t size, const pm
             parser->suppress_warnings = true;
         }
 
+        // version option
+        parser->version = options->version;
+
         // scopes option
         for (size_t scope_index = 0; scope_index < options->scopes_count; scope_index++) {
             const pm_options_scope_t *scope = pm_options_scope_get(options, scope_index);


Test patch:
diff --git a/test/prism/snapshots/arrays.txt b/test/prism/snapshots/arrays.txt
index 93f5c8fb..40d4cbed 100644
--- a/test/prism/snapshots/arrays.txt
+++ b/test/prism/snapshots/arrays.txt
@@ -430,7 +430,7 @@
         ├── @ MultiWriteNode (location: (41,0)-(41,21))
         │   ├── lefts: (length: 2)
         │   │   ├── @ IndexTargetNode (location: (41,0)-(41,6))
-        │   │   │   ├── flags: ∅
+        │   │   │   ├── flags: attribute_write
         │   │   │   ├── receiver:
         │   │   │   │   @ CallNode (location: (41,0)-(41,3))
         │   │   │   │   ├── flags: variable_call
@@ -452,7 +452,7 @@
         │   │   │   ├── closing_loc: (41,5)-(41,6) = "]"
         │   │   │   └── block: ∅
         │   │   └── @ IndexTargetNode (location: (41,8)-(41,14))
-        │   │       ├── flags: ∅
+        │   │       ├── flags: attribute_write
         │   │       ├── receiver:
         │   │       │   @ CallNode (location: (41,8)-(41,11))
         │   │       │   ├── flags: variable_call
@@ -2166,7 +2166,7 @@
         │   │   │   ├── operator_loc: (140,17)-(140,19) = "=>"
         │   │   │   ├── reference:
         │   │   │   │   @ IndexTargetNode (location: (140,20)-(140,24))
-        │   │   │   │   ├── flags: ∅
+        │   │   │   │   ├── flags: attribute_write
         │   │   │   │   ├── receiver:
         │   │   │   │   │   @ CallNode (location: (140,20)-(140,21))
         │   │   │   │   │   ├── flags: variable_call
@@ -2229,7 +2229,7 @@
             │   │   ├── operator_loc: (142,17)-(142,19) = "=>"
             │   │   ├── reference:
             │   │   │   @ IndexTargetNode (location: (142,20)-(142,27))
-            │   │   │   ├── flags: ∅
+            │   │   │   ├── flags: attribute_write
             │   │   │   ├── receiver:
             │   │   │   │   @ CallNode (location: (142,20)-(142,21))
             │   │   │   │   ├── flags: variable_call
diff --git a/test/prism/snapshots/unparser/corpus/literal/assignment.txt b/test/prism/snapshots/unparser/corpus/literal/assignment.txt
index 18edafe4..4cb19d28 100644
--- a/test/prism/snapshots/unparser/corpus/literal/assignment.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/assignment.txt
@@ -319,7 +319,7 @@
         ├── @ MultiWriteNode (location: (15,0)-(15,24))
         │   ├── lefts: (length: 2)
         │   │   ├── @ IndexTargetNode (location: (15,1)-(15,8))
-        │   │   │   ├── flags: ∅
+        │   │   │   ├── flags: attribute_write
         │   │   │   ├── receiver:
         │   │   │   │   @ LocalVariableReadNode (location: (15,1)-(15,2))
         │   │   │   │   ├── name: :a
@@ -338,7 +338,7 @@
         │   │   │   ├── closing_loc: (15,7)-(15,8) = "]"
         │   │   │   └── block: ∅
         │   │   └── @ IndexTargetNode (location: (15,10)-(15,14))
-        │   │       ├── flags: ∅
+        │   │       ├── flags: attribute_write
         │   │       ├── receiver:
         │   │       │   @ LocalVariableReadNode (location: (15,10)-(15,11))
         │   │       │   ├── name: :a
@@ -370,7 +370,7 @@
         ├── @ MultiWriteNode (location: (16,0)-(16,21))
         │   ├── lefts: (length: 2)
         │   │   ├── @ IndexTargetNode (location: (16,1)-(16,5))
-        │   │   │   ├── flags: ∅
+        │   │   │   ├── flags: attribute_write
         │   │   │   ├── receiver:
         │   │   │   │   @ LocalVariableReadNode (location: (16,1)-(16,2))
         │   │   │   │   ├── name: :a
@@ -385,7 +385,7 @@
         │   │   │   ├── closing_loc: (16,4)-(16,5) = "]"
         │   │   │   └── block: ∅
         │   │   └── @ IndexTargetNode (location: (16,7)-(16,11))
-        │   │       ├── flags: ∅
+        │   │       ├── flags: attribute_write
         │   │       ├── receiver:
         │   │       │   @ LocalVariableReadNode (location: (16,7)-(16,8))
         │   │       │   ├── name: :a
diff --git a/test/prism/snapshots/whitequark/masgn_attr.txt b/test/prism/snapshots/whitequark/masgn_attr.txt
index d87d7a6c..2a4dc38b 100644
--- a/test/prism/snapshots/whitequark/masgn_attr.txt
+++ b/test/prism/snapshots/whitequark/masgn_attr.txt
@@ -34,7 +34,7 @@
         │   │   │   ├── name: :a=
         │   │   │   └── message_loc: (3,5)-(3,6) = "a"
         │   │   └── @ IndexTargetNode (location: (3,8)-(3,18))
-        │   │       ├── flags: ∅
+        │   │       ├── flags: attribute_write
         │   │       ├── receiver:
         │   │       │   @ SelfNode (location: (3,8)-(3,12))
         │   │       ├── opening_loc: (3,12)-(3,13) = "["