Skip to content

Commit 54652c1

Browse files
authored
Merge pull request #7803 from processing/rest-types
Rest types
2 parents b990850 + 425e456 commit 54652c1

File tree

6 files changed

+66
-18
lines changed

6 files changed

+66
-18
lines changed

docs/parameterData.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1427,7 +1427,7 @@
14271427
"createVector": {
14281428
"overloads": [
14291429
[
1430-
null
1430+
"...Number[]"
14311431
]
14321432
]
14331433
},

src/core/friendly_errors/param_validator.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,14 @@ function validateParams(p5, fn, lifecycles) {
226226
const isOptional = param?.endsWith('?');
227227
param = param?.replace(/\?$/, '');
228228

229-
let schema = generateTypeSchema(param);
229+
const isRest = param?.startsWith('...') && param?.endsWith('[]');
230+
param = param?.replace(/^\.\.\.(.+)\[\]$/, '$1');
230231

231-
return isOptional ? schema.optional() : schema;
232+
let schema = generateTypeSchema(param);
233+
if (isOptional) {
234+
schema = schema.optional();
235+
}
236+
return { schema, rest: isRest };
232237
};
233238

234239
// Note that in Zod, `optional()` only checks for undefined, not the absence
@@ -262,14 +267,22 @@ function validateParams(p5, fn, lifecycles) {
262267
const overloadSchemas = overloads.flatMap(overload => {
263268
const combinations = generateOverloadCombinations(overload);
264269

265-
return combinations.map(combo =>
266-
z.tuple(
267-
combo
268-
.map(p => generateParamSchema(p))
269-
// For now, ignore schemas that cannot be mapped to a defined type
270-
.filter(schema => schema !== undefined)
271-
)
272-
);
270+
return combinations.map(combo => {
271+
const params = combo
272+
.map(p => generateParamSchema(p))
273+
.filter(s => s.schema !== undefined);
274+
275+
let rest;
276+
if (params.at(-1)?.rest) {
277+
rest = params.pop();
278+
}
279+
280+
let combined = z.tuple(params.map(s => s.schema));
281+
if (rest) {
282+
combined = combined.rest(rest.schema);
283+
}
284+
return combined;
285+
});
273286
});
274287

275288
return overloadSchemas.length === 1
@@ -504,7 +517,7 @@ function validateParams(p5, fn, lifecycles) {
504517
// theoretically allowed to stay undefined and valid, it is likely that the
505518
// user intended to call the function with non-undefined arguments. Skip
506519
// regular workflow and return a friendly error message right away.
507-
if (Array.isArray(args) && args.every(arg => arg === undefined)) {
520+
if (Array.isArray(args) && args.length > 0 && args.every(arg => arg === undefined)) {
508521
const undefinedErrorMessage = `🌸 p5.js says: All arguments for ${func}() are undefined. There is likely an error in the code.`;
509522

510523
return {

src/math/math.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function math(p5, fn) {
3434
* <a href="#/p5.Vector">p5.Vector</a> class.
3535
*
3636
* @method createVector
37-
* @param {...Number} components Components of the vector.
37+
* @param {...Number} x Zero or more numbers, representing each component of the vector.
3838
* @return {p5.Vector} new <a href="#/p5.Vector">p5.Vector</a> object.
3939
*
4040
* @example

test/unit/core/param_errors.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ suite('Validate Params', function () {
253253
];
254254
const result = mockP5Prototype.validate('p5.paletteLerp', [colorStops, 0.5]);
255255
assert.isTrue(result.success);
256-
})
257-
})
256+
});
257+
});
258+
259+
suite('validateParams: rest arguments', function () {
260+
test('createVector(): works with no args', function() {
261+
const result = mockP5Prototype.validate('p5.createVector', []);
262+
assert.isTrue(result.success);
263+
});
264+
test('createVector(): works with one number', function() {
265+
const result = mockP5Prototype.validate('p5.createVector', [1]);
266+
assert.isTrue(result.success);
267+
});
268+
test('createVector(): works with many numbers', function() {
269+
const result = mockP5Prototype.validate('p5.createVector', [1, 2, 3, 4]);
270+
assert.isTrue(result.success);
271+
});
272+
test('createVector(): fails with a non-number', function() {
273+
const result = mockP5Prototype.validate('p5.createVector', ['1']);
274+
assert.isFalse(result.success);
275+
});
276+
test('createVector(): fails with any non-number', function() {
277+
const result = mockP5Prototype.validate('p5.createVector', [1, 2, '3', 4]);
278+
assert.isFalse(result.success);
279+
});
280+
});
258281
});

utils/convert.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ function typeObject(node) {
9292
return { type: signature };
9393
} else if (node.type === 'ArrayType') {
9494
return { type: `[${node.elements.map(e => typeObject(e).type).join(', ')}]` };
95+
} else if (node.type === 'RestType') {
96+
return { type: typeObject(node.expression).type, rest: true };
9597
} else {
9698
// TODO
9799
// - handle record types
@@ -518,19 +520,22 @@ function cleanUpClassItems(data) {
518520

519521
const processOverload = overload => {
520522
if (overload.params) {
521-
return Object.values(overload.params).map(param => processOptionalParam(param));
523+
return Object.values(overload.params).map(param => processParam(param));
522524
}
523525
return overload;
524526
}
525527

526528
// To simplify `parameterData.json`, instead of having a separate field for
527529
// optional parameters, we'll add a ? to the end of parameter type to
528530
// indicate that it's optional.
529-
const processOptionalParam = param => {
531+
const processParam = param => {
530532
let type = param.type;
531533
if (param.optional) {
532534
type += '?';
533535
}
536+
if (param.rest) {
537+
type = `...${type}[]`;
538+
}
534539
return type;
535540
}
536541

utils/helper.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ export function generateTypeFromTag(param) {
392392
const innerTypeStrs = param.type.elements.map(e => generateTypeFromTag({ type: e }));
393393
return `[${innerTypeStrs.join(', ')}]`;
394394
}
395+
case 'RestType':
396+
return `${generateTypeFromTag({ type: param.type.expression })}[]`;
395397
default:
396398
return 'any';
397399
}
@@ -420,6 +422,7 @@ export function generateTypeFromTag(param) {
420422
if (!param) return 'any';
421423

422424
let type = param.type;
425+
let prefix = '';
423426
const isOptional = param.type?.type === 'OptionalType';
424427
if (typeof type === 'string') {
425428
type = normalizeTypeName(type);
@@ -429,7 +432,11 @@ export function generateTypeFromTag(param) {
429432
type = 'any';
430433
}
431434

432-
return `${param.name}${isOptional ? '?' : ''}: ${type}`;
435+
if (param.type?.type === 'RestType') {
436+
prefix = '...';
437+
}
438+
439+
return `${prefix}${param.name}${isOptional ? '?' : ''}: ${type}`;
433440
}
434441

435442
export function generateFunctionDeclaration(funcDoc) {

0 commit comments

Comments
 (0)