Skip to content

Commit 589cf75

Browse files
authored
Support @template (#153)
* Add/implement parseJSDocTemplates.js * partition.js: choose unique name for arr in loop * src-transpiler/parseJSDocTemplates.js: make function type simply CapitalCase * src-transpiler/parseJSDoc.js: Improve typing * src-transpiler/index.js: export parseJSDocTemplates.js * replaceType: handle arrays and unions * Add unit tests * ArrayPattern unit test changes * Add test/typechecking/simple-ArrayPattern-output.mjs * Update test/typechecking.json * Integrate parseJSDocTemplates in src-transpiler/Asserter.js * Add inspectTypeWithTemplates.js * src-runtime/createType.js: Fix spelling * Add test/typechecking/template-types-1.js * Integrate as proper unit test: test/typechecking/template-types-1
1 parent fc5d3f3 commit 589cf75

17 files changed

+425
-62
lines changed

src-runtime/createType.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {createTypeFromMapping } from "./createTypeFromMapping.js";
55
/**
66
* @param {string|import('./validateType.js').Type} expect - The supposed type information of said value.
77
* @param {console["warn"]} warn - Function to warn with.
8-
* @returns {import('./validateType.js').Type|undefined} - New type that can be used for validatoin
8+
* @returns {import('./validateType.js').Type|undefined} - New type that can be used for validation.
99
*/
1010
function createType(expect, warn) {
1111
const mapping = resolveType(expect, 'mapping', warn);

src-runtime/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './customTypes.js';
77
export * from './customValidations.js';
88
export * from './getTypeKeys.js';
99
export * from './inspectType.js';
10+
export * from './inspectTypeWithTemplates.js';
1011
export * from './isObject.js';
1112
export * from './makeJSDoc.js';
1213
export * from './options.js';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {inspectType} from "./inspectType.js";
2+
import {replaceType} from "./replaceType.js";
3+
function inspectTypeWithTemplates(value, expect, loc, name, templates) {
4+
// console.log("old expect", expect);
5+
for (const key in templates) {
6+
expect = replaceType(expect, key, templates[key], console.warn);
7+
}
8+
// console.log("new expect", expect);
9+
const ret = inspectType(value, expect, loc, name);
10+
// if (ret) {
11+
// Narrowing string|number down, for instance:
12+
// templates["T"] = "number";
13+
// We don't narrow down the possible template type based on first input type here,
14+
// because after all it could be a union of all types. We can't know that from
15+
// JS side. However I still like the idea of narrowing it down, so we
16+
// could at least make it an option... and obviously we also have to integrate
17+
// the NoInfer intrinsic, but that will be later PR's...
18+
// }
19+
return ret;
20+
}
21+
export {inspectTypeWithTemplates};

src-runtime/partition.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ function partition(arr, fn) {
1414
/** @type {any[]} */
1515
const fail = [];
1616
for (const element of arr) {
17-
const arr = fn(element) ? pass : fail;
18-
arr.push(element);
17+
const pick = fn(element) ? pass : fail;
18+
pick.push(element);
1919
}
2020
return [pass, fail];
2121
}

src-runtime/replaceType.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* - add bunch of unit tests to test all possible cases
66
* - call it copyAndReplace?
77
* - if a subtype is a typedef and we change that without creating a copy, we invalidate that typedef... make a unit test
8+
* - add intersection type replacements for instance: 'fixed' & Key
89
* @param {*} type - The type.
910
* @param {*} search - The search.
1011
* @param {*} replace - The replace.
@@ -25,14 +26,23 @@ function replaceType(type, search, replace, warn) {
2526
properties[prop] = replaceType(val, search, replace, warn);
2627
}
2728
return type;
28-
} else if (type.type === "tuple") {
29+
} else if (type.type === 'tuple') {
2930
const {elements} = type;
30-
const n = elements.length;
31-
for (let i = 0; i < n; i++) {
32-
const val = elements[i];
33-
const replacedVal = replaceType(val, search, replace, warn);
34-
elements[i] = replacedVal;
35-
//if ()
31+
const {length } = elements;
32+
for (let i = 0; i < length; i++) {
33+
const element = elements[i];
34+
elements[i] = replaceType(element, search, replace, warn);
35+
}
36+
return type;
37+
} else if (type.type === 'array') {
38+
type.elementType = replaceType(type.elementType, search, replace, warn);
39+
return type;
40+
} else if (type.type === 'union') {
41+
const {members} = type;
42+
const {length } = members;
43+
for (let i = 0; i < length; i++) {
44+
const member = members[i];
45+
members[i] = replaceType(member, search, replace, warn);
3646
}
3747
return type;
3848
}

src-transpiler/Asserter.js

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import {requiredTypeofs } from './expandType.js';
2-
import {expandTypeDepFree} from './expandTypeDepFree.js';
3-
import {nodeIsFunction } from './nodeIsFunction.js';
4-
import {parseJSDoc } from './parseJSDoc.js';
5-
import {parseJSDocSetter } from './parseJSDocSetter.js';
6-
import {parseJSDocTypedef} from './parseJSDocTypedef.js';
7-
import {statReset } from './stat.js';
8-
import {Stringifier } from './Stringifier.js';
1+
import {requiredTypeofs } from './expandType.js';
2+
import {expandTypeDepFree } from './expandTypeDepFree.js';
3+
import {nodeIsFunction } from './nodeIsFunction.js';
4+
import {parseJSDoc } from './parseJSDoc.js';
5+
import {parseJSDocSetter } from './parseJSDocSetter.js';
6+
import {parseJSDocTemplates} from './parseJSDocTemplates.js';
7+
import {parseJSDocTypedef } from './parseJSDocTypedef.js';
8+
import {statReset } from './stat.js';
9+
import {Stringifier } from './Stringifier.js';
910
/** @typedef {import('@babel/types').Node } Node */
10-
/** @typedef {import("@babel/types").ClassMethod } ClassMethod */
11-
/** @typedef {import("@babel/types").ClassPrivateMethod} ClassPrivateMethod */
12-
/** @typedef {import('./stat.js').Stat } Stat */
11+
/** @typedef {import('@babel/types').ClassMethod } ClassMethod */
12+
/** @typedef {import('@babel/types').ClassPrivateMethod} ClassPrivateMethod */
13+
/** @typedef {import('./stat.js').Stat } Stat */
1314
/**
1415
* @typedef {object} Options
1516
* @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
1617
* @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
17-
* @property {Function} [expandType] - A function that expands shorthand types into full descriptions.
18+
* @property {import('./parseJSDoc.js').ExpandType} [expandType] - A function that expands shorthand types into full descriptions.
1819
* @property {string} [filename] - The name of a file to which the instance pertains.
1920
* @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
2021
* @property {string[]} [ignoreLocations] - Ignore these locations because they are known false-positives.
@@ -219,9 +220,9 @@ class Asserter extends Stringifier {
219220
}
220221
/**
221222
* @param {Node} node - The Babel AST node.
222-
* @returns {undefined | {}} The return value of `parseJSDoc`.
223+
* @returns {string|undefined} The JSDoc comment of `node`.
223224
*/
224-
getJSDoc(node) {
225+
getLeadingComment(node) {
225226
if (node.type === 'BlockStatement') {
226227
node = this.parent;
227228
}
@@ -245,26 +246,48 @@ class Asserter extends Stringifier {
245246
if (leadingComments && leadingComments.length) {
246247
const lastComment = leadingComments[leadingComments.length - 1];
247248
if (lastComment.type === "CommentBlock") {
248-
if (lastComment.value.includes('@event')) {
249-
return;
250-
}
251-
if (node.type === 'ClassMethod' && node.kind === 'set') {
252-
const paramName = this.getNameOfParam(node.params[0]);
253-
if (node.params.length !== 1) {
254-
this.warn("getJSDoc> setters require exactly one argument");
255-
}
256-
const setterType = parseJSDocSetter(lastComment.value, this.expandType);
257-
if (!setterType) {
258-
return;
259-
}
260-
return {[paramName]: setterType};
261-
}
262-
if (lastComment.value.includes('@ignoreRTI')) {
263-
return;
264-
}
265-
return parseJSDoc(lastComment.value, this.expandType);
249+
return lastComment.value;
250+
}
251+
}
252+
}
253+
/**
254+
* @param {Node} node - The Babel AST node.
255+
* @todo ESLint problem:
256+
* returns {import('./parseJSDoc.js').ParseJSDocReturnType} The return value of `parseJSDoc`
257+
* returns {Record<string, import('./parseJSDoc.js').ExpandTypeReturnType> | undefined} The
258+
* return value of `parseJSDoc`.
259+
* @returns {Record<string, any>|undefined} asd
260+
*/
261+
getJSDoc(node) {
262+
const comment = this.getLeadingComment(node);
263+
if (!comment) {
264+
return;
265+
}
266+
if (comment.includes('@event')) {
267+
return;
268+
}
269+
if (comment.includes('@ignoreRTI')) {
270+
return;
271+
}
272+
// Need to do same resolving as in: this.getLeadingComment(node)
273+
if (node.type === 'BlockStatement') {
274+
node = this.parent;
275+
}
276+
if (node.type === 'ClassMethod' && node.kind === 'set') {
277+
const paramName = this.getNameOfParam(node.params[0]);
278+
if (node.params.length !== 1) {
279+
this.warn("getJSDoc> setters require exactly one argument");
280+
}
281+
const setterType = parseJSDocSetter(comment, this.expandType);
282+
if (!setterType) {
283+
return;
266284
}
285+
const params = {[paramName]: setterType};
286+
return {templates: undefined, params};
267287
}
288+
const templates = parseJSDocTemplates(comment);
289+
const params = parseJSDoc(comment, this.expandType);
290+
return {templates, params};
268291
}
269292
/**
270293
* Retrieves the name of a parameter from a Babel AST node.
@@ -283,8 +306,7 @@ class Asserter extends Stringifier {
283306
return param.left.name;
284307
}
285308
}
286-
debugger;
287-
this.warn("unable to extra name from param in specified way - may contain too much information");
309+
this.warn("Unable to retrieve name from param in specified way - may contain too much information.");
288310
return this.toSource(param);
289311
}
290312
statsReset() {
@@ -391,6 +413,12 @@ class Asserter extends Stringifier {
391413
stat.unchecked++;
392414
return '';
393415
}
416+
const {templates, params} = jsdoc;
417+
if (!params) {
418+
console.warn("This should never happen, please check your input code.", this.getLeadingComment(node), {jsdoc});
419+
stat.unchecked++;
420+
return '';
421+
}
394422
stat.checked++;
395423
const {spaces} = this;
396424
let out = '';
@@ -399,17 +427,21 @@ class Asserter extends Stringifier {
399427
if (this.ignoreLocations.includes(loc)) {
400428
return '// IGNORE RTI TYPE VALIDATIONS, KNOWN ISSUES\n';
401429
}
430+
if (templates) {
431+
const tmp = JSON.stringify(templates, null, 2).replaceAll('\n', '\n' + spaces);
432+
out += `\n${spaces}const rtiTemplates = ${tmp};`;
433+
}
402434
//out += `${spaces}/*${spaces} node.type=${node.type}\n${spaces}
403435
// ${JSON.stringify(jsdoc)}\n${parent}\n${spaces}*/\n`;
404-
for (let name in jsdoc) {
405-
const type = jsdoc[name];
436+
for (let name in params) {
437+
const type = params[name];
406438
const hasParam = this.nodeHasParamName(node, name);
407439
if (!hasParam) {
408440
let testNode = node;
409441
if (node.type === 'BlockStatement') {
410442
testNode = this.parent;
411443
}
412-
const paramIndex = Object.keys(jsdoc).findIndex(_ => _ === name);
444+
const paramIndex = Object.keys(params).findIndex(_ => _ === name);
413445
const param = testNode.params[paramIndex];
414446
if (param) {
415447
const isObjectPattern = param.type === 'ObjectPattern';
@@ -439,7 +471,11 @@ class Asserter extends Stringifier {
439471
continue;
440472
}
441473
const t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
442-
out += `${spaces}if (!inspectType(${element.name}, ${t}, '${loc}', '${name}')) {\n`;
474+
if (templates) {
475+
out += `${spaces}if (!inspectTypeWithTemplates(${element.name}, ${t}, '${loc}', '${name}', rtiTemplates)) {\n`;
476+
} else {
477+
out += `${spaces}if (!inspectType(${element.name}, ${t}, '${loc}', '${name}')) {\n`;
478+
}
443479
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
444480
}
445481
continue;
@@ -465,7 +501,11 @@ class Asserter extends Stringifier {
465501
continue;
466502
}
467503
const t = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
468-
out += `${spaces}if (!inspectType(${keyName}, ${t}, '${loc}', '${name}')) {\n`;
504+
if (templates) {
505+
out += `${spaces}if (!inspectTypeWithTemplates(${keyName}, ${t}, '${loc}', '${name}', rtiTemplates)) {\n`;
506+
} else {
507+
out += `${spaces}if (!inspectType(${keyName}, ${t}, '${loc}', '${name}')) {\n`;
508+
}
469509
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
470510
}
471511
continue;
@@ -503,7 +543,11 @@ class Asserter extends Stringifier {
503543
out += '\n';
504544
first = false;
505545
}
506-
out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${loc}', '${name}')) {\n`;
546+
if (templates) {
547+
out += `${spaces}if (${prevCheck}!inspectTypeWithTemplates(${name}, ${t}, '${loc}', '${name}', rtiTemplates)) {\n`;
548+
} else {
549+
out += `${spaces}if (${prevCheck}!inspectType(${name}, ${t}, '${loc}', '${name}')) {\n`;
550+
}
507551
out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
508552
}
509553
return out;

src-transpiler/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './extractNameAndOptionality.js';
1717
export * from './nodeIsFunction.js';
1818
export * from './parseJSDoc.js';
1919
export * from './parseJSDocSetter.js';
20+
export * from './parseJSDocTemplates.js';
2021
export * from './parseJSDocTypedef.js';
2122
export * from './simplifyType.js';
2223
export * from './Stringifier.js';

src-transpiler/parseJSDoc.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import {expandTypeDepFree} from "./expandTypeDepFree.js";
22
import {simplifyType} from "./simplifyType.js";
3+
/**
4+
* @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
5+
*/
6+
/**
7+
* @typedef {typeof expandTypeDepFree} ExpandType
8+
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
9+
*/
310
/**
411
* Parses JSDoc comments to extract parameter type information.
512
*
613
* @param {string} src - The JSDoc comment string to parse.
7-
* @param {Function} [expandType] - An optional function to process the types found in the JSDoc.
8-
* @returns {Record<string, any> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
14+
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
15+
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
916
*/
1017
function parseJSDoc(src, expandType = expandTypeDepFree) {
1118
// Parse something like: @param {Object} [kwargs={}] Optional arguments.
1219
const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\{\}\.'" ]+)/g;
1320
const matches = [...src.matchAll(regex)];
14-
/** @type {Record<string, any>} */
21+
/** @type {Record<string, ExpandTypeReturnType>} */
1522
const params = Object.create(null);
1623
matches.forEach(_ => {
1724
const type = expandType(_[1].trim());
@@ -35,7 +42,6 @@ function parseJSDoc(src, expandType = expandTypeDepFree) {
3542
const parts = name.split(/[\[\]]*\./);
3643
let properties = params;
3744
for (const part of parts) {
38-
/** @type {object} */
3945
const toptype = properties[part];
4046
if (!toptype) {
4147
// No toptype means we resolved as far as possible, now we can add `simplifiedType`.

src-transpiler/parseJSDocTemplates.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {expandTypeDepFree} from "./expandTypeDepFree.js";
2+
/**
3+
* @typedef {typeof expandTypeDepFree} ExpandType
4+
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
5+
*/
6+
/**
7+
* Parses JSDoc comments to extract parameter type information.
8+
*
9+
* @param {string} src - The JSDoc comment string to parse.
10+
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
11+
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping template names to their parsed types,
12+
* or `undefined` if no template tags were found.
13+
*/
14+
function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
15+
const regexTemplateTyped = /@template \{(.*?)\} ([a-zA-Z0-9_$=]+)/g;
16+
const matches = [...src.matchAll(regexTemplateTyped)];
17+
if (!matches.length) {
18+
return;
19+
}
20+
/** @type {Record<string, ExpandTypeReturnType>} */
21+
const templates = Object.create(null);
22+
matches.forEach(_ => {
23+
const type = expandType(_[1].trim());
24+
const name = _[2].trim();
25+
templates[name] = type;
26+
});
27+
return templates;
28+
}
29+
export {parseJSDocTemplates};

test/replaceType/1.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Mode: eval
2+
registerTypedef('TestObject', expandType(`{a: 'aa', b: 'bb', c: 'cc'}`));
3+
console.log("typedefs", typedefs);
4+
const mapping = expandType(`{
5+
[Key in 1|2]: {
6+
td: TestObject,
7+
testkey: [Key, Key, Key],
8+
bla: Key,
9+
testArr: Key[],
10+
testUnion: 'fixed'|Key,
11+
}
12+
}`);
13+
const type = createTypeFromMapping(mapping, console.warn);
14+
const key = type.properties[1].properties.bla;
15+
console.log(`The key should be 1: ${key} (${key === 1})`, {type});
16+
function stringify(_) {
17+
let ret = JSON.stringify(_, null, 2);
18+
const regExp = /\[[\n ,0-9]*?\]/gm;
19+
ret = ret.replace(regExp, (all) => {
20+
return all.replace(/\n| /g, '');
21+
});
22+
// Turn this: "type": "array"
23+
// Into this: type: "array"
24+
ret = ret.replaceAll(/\"([a-zA-Z0-9]+)\":+/g, "$1: ");
25+
return ret;
26+
}
27+
function a() {
28+
//const type = expandType("number|T");
29+
//const type = expandType("[T, T, T]");
30+
const type = expandType("T[]");
31+
//const type = expandType("string");
32+
const newType = replaceType(type, 'T', 'string', console.warn);
33+
return stringify(newType);
34+
}
35+
setRight(stringify(type) + '\n\n' + a());

0 commit comments

Comments
 (0)