@@ -37,6 +37,8 @@ type ConnectionMap = Record<
3737/**
3838 * Possible types for procedure arguments.
3939 */
40+ type ArgumentDefault = string | number | boolean | null
41+
4042enum ArgumentType {
4143 STRING = 's' ,
4244 NUMBER = 'n' ,
@@ -75,39 +77,17 @@ function getRequiredMutationAttribute(xmlElement: Element, name: string): string
7577 return value
7678}
7779
78- /**
79- * Parse a required mutation attribute as JSON, then validate its type.
80- * @param xmlElement The mutation element that should contain required attributes.
81- * @param name The specific mutation attribute to retrieve and parse.
82- * @param parse Validates and narrows the parsed JSON value.
83- * @returns Parsed and validated mutation attribute value.
84- */
85- function parseRequiredMutationJson < T > (
86- xmlElement : Element ,
87- name : string ,
88- parse : ( value : unknown , name : string ) => T ,
89- ) : T {
90- const rawValue = getRequiredMutationAttribute ( xmlElement , name )
91- let parsedValue : unknown
92- try {
93- parsedValue = JSON . parse ( rawValue )
94- } catch {
95- throw new Error ( `Invalid JSON in mutation attribute: ${ name } ` )
96- }
97- return parse ( parsedValue , name )
98- }
99-
10080/**
10181 * Parse an optional mutation attribute as JSON, returning a fallback when the
102- * attribute is absent. Use this only for attributes that can be safely
103- * defaulted in isolation, without invalidating structural invariants that
104- * relate them to other attributes on the same mutation. A present-but-malformed
105- * attribute still throws, since that indicates corruption rather than an older
106- * schema .
82+ * attribute is absent or malformed. Older saved projects may omit attributes
83+ * that current serialization always writes, and some projects in the wild
84+ * contain attributes whose values are not valid for the expected type.
85+ * Returning the fallback in both cases lets the project load rather than
86+ * aborting the entire workspace parse .
10787 * @param xmlElement The mutation element that may contain the attribute.
10888 * @param name The specific mutation attribute to retrieve and parse.
10989 * @param parse Validates and narrows the parsed JSON value.
110- * @param fallback Value to return when the attribute is absent.
90+ * @param fallback Value to return when the attribute is absent or malformed .
11191 * @returns Parsed and validated mutation attribute value, or `fallback`.
11292 */
11393function parseOptionalMutationJson < T > (
@@ -120,13 +100,12 @@ function parseOptionalMutationJson<T>(
120100 if ( rawValue === null ) {
121101 return fallback
122102 }
123- let parsedValue : unknown
124103 try {
125- parsedValue = JSON . parse ( rawValue )
104+ return parse ( JSON . parse ( rawValue ) , name )
126105 } catch {
127- throw new Error ( `Invalid JSON in mutation attribute: ${ name } ` )
106+ console . warn ( `Invalid or unexpected mutation attribute "${ name } ", using default. Raw value: ${ rawValue } ` )
107+ return fallback
128108 }
129- return parse ( parsedValue , name )
130109}
131110
132111/**
@@ -155,6 +134,26 @@ function parseStringArrayMutationValue(value: unknown, name: string): string[] {
155134 return value
156135}
157136
137+ /**
138+ * Validate a parsed mutation value as an array of primitives. Argument
139+ * defaults from older projects (especially Scratch 2.0 conversions) contain
140+ * mixed types — strings for text arguments, numbers for numeric arguments,
141+ * and booleans for boolean arguments (e.g. `["",1,1,1,1]`). The old fork
142+ * stored these via untyped `JSON.parse` with no validation, and the VM
143+ * passes them through as-is.
144+ * @param value Parsed mutation value.
145+ * @param name Attribute name used in error messages.
146+ * @returns Validated array value.
147+ */
148+ function parsePrimitiveArrayMutationValue ( value : unknown , name : string ) : ArgumentDefault [ ] {
149+ const isArgumentDefault = ( entry : unknown ) : entry is ArgumentDefault =>
150+ entry === null || typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean'
151+ if ( ! Array . isArray ( value ) || ! value . every ( isArgumentDefault ) ) {
152+ throw new Error ( `Expected ArgumentDefault[] JSON value in mutation attribute: ${ name } ` )
153+ }
154+ return value
155+ }
156+
158157/**
159158 * A drag strategy for the procedures_prototype block that delegates all drag
160159 * operations to its parent (the procedures_definition block). This lets the
@@ -308,7 +307,7 @@ function callerDomToMutation(this: ProcedureCallBlock, xmlElement: Element) {
308307 this . procCode_ = getRequiredMutationAttribute ( xmlElement , 'proccode' )
309308 const generateshadows = xmlElement . getAttribute ( 'generateshadows' )
310309 this . generateShadows_ = generateshadows !== null ? JSON . parse ( generateshadows ) === true : false
311- this . argumentIds_ = parseRequiredMutationJson ( xmlElement , 'argumentids' , parseStringArrayMutationValue )
310+ this . argumentIds_ = parseOptionalMutationJson ( xmlElement , 'argumentids' , parseStringArrayMutationValue , [ ] )
312311 this . warp_ = parseOptionalMutationJson ( xmlElement , 'warp' , parseBooleanMutationValue , false )
313312 this . updateDisplay_ ( )
314313}
@@ -349,9 +348,14 @@ function definitionDomToMutation(this: ProcedurePrototypeBlock | ProcedureDeclar
349348 const prevArgIds = this . argumentIds_
350349 const prevDisplayNames = this . displayNames_
351350
352- this . argumentIds_ = parseRequiredMutationJson ( xmlElement , 'argumentids' , parseStringArrayMutationValue )
353- this . displayNames_ = parseRequiredMutationJson ( xmlElement , 'argumentnames' , parseStringArrayMutationValue )
354- this . argumentDefaults_ = parseRequiredMutationJson ( xmlElement , 'argumentdefaults' , parseStringArrayMutationValue )
351+ this . argumentIds_ = parseOptionalMutationJson ( xmlElement , 'argumentids' , parseStringArrayMutationValue , [ ] )
352+ this . displayNames_ = parseOptionalMutationJson ( xmlElement , 'argumentnames' , parseStringArrayMutationValue , [ ] )
353+ this . argumentDefaults_ = parseOptionalMutationJson (
354+ xmlElement ,
355+ 'argumentdefaults' ,
356+ parsePrimitiveArrayMutationValue ,
357+ [ ] ,
358+ )
355359
356360 // During full XML deserialization (Blockly.Xml.domToWorkspace), the mutation element
357361 // is part of the parsed XML tree and its parent element also contains <value> children
@@ -1341,7 +1345,7 @@ interface ProcedureBlock extends Blockly.BlockSvg {
13411345
13421346export interface ProcedureDeclarationBlock extends ProcedureBlock {
13431347 displayNames_ : string [ ]
1344- argumentDefaults_ : string [ ]
1348+ argumentDefaults_ : ArgumentDefault [ ]
13451349 removeFieldCallback : ( field : Blockly . Field ) => void
13461350 createArgumentEditor_ : ( argumentType : ArgumentType , displayName : string ) => Blockly . BlockSvg
13471351 focusLastEditor_ : ( ) => void
@@ -1361,7 +1365,7 @@ interface ProcedureCallBlock extends ProcedureBlock {
13611365
13621366interface ProcedurePrototypeBlock extends ProcedureBlock {
13631367 displayNames_ : string [ ]
1364- argumentDefaults_ : string [ ]
1368+ argumentDefaults_ : ArgumentDefault [ ]
13651369 skipArgumentReporters_ : boolean
13661370 createArgumentReporter_ : ( argumentType : ArgumentType , displayName : string ) => Blockly . BlockSvg
13671371 updateArgumentReporterNames_ : ( prevArgIds : string [ ] , prevDisplayNames : string [ ] ) => void
0 commit comments