diff --git a/lib/collector.js b/lib/collector.js index 9e9f090..ab2a013 100644 --- a/lib/collector.js +++ b/lib/collector.js @@ -10,6 +10,7 @@ const Command = require('./commands'); const Module = require('./module'); const Scope = require('./scope'); const CircularList = require('./list'); +const {isNode} = require('./utils'); class Collector { constructor(parser, root = '.') { @@ -46,7 +47,7 @@ class Collector { const scope = this.global.extend(module); - this._freestyle(extractors.declaration, ast.program, scope); + this._freestyle(extractors.declaration, ast.program, scope, null); this.modules.set(path, module); @@ -69,7 +70,7 @@ class Collector { } // Given the AST output of babylon parse, walk through in a depth-first order. - _freestyle(group, root, scope) { + _freestyle(group, root, scope, params) { let stack; let parent; let keys = []; @@ -91,7 +92,7 @@ class Collector { if (isNode(node) && isAcceptableGroup(group, node)) { if (!this.roots.has(node)) { - const task = this._collect(group, node, scope); + const task = this._collect(group, node, scope, params); this.roots.add(node); this._spawn(task); } @@ -108,11 +109,11 @@ class Collector { } while (stack); } - * _collect(group, node, scope) { + * _collect(group, node, scope, params) { const extractor = group[node.type]; if (!extractor) { - this._freestyle(group, node, scope); + this._freestyle(group, node, scope, null); return null; } @@ -138,8 +139,19 @@ class Collector { break; case 'define': - scope.addDefinition(...value.data); - this.schemas.push(value.data[0]); + const [schema, declared] = value.data; + + if (declared && params) { + const name = schema.name; + + schema.name = generateGenericName(name, params); + + scope.addInstance(name, schema, params.map(p => p.value)); + } else { + scope.addDefinition(...value.data); + } + + this.schemas.push(schema); break; case 'external': @@ -151,7 +163,16 @@ class Collector { break; case 'query': - result = yield* this._query(scope, value.data); + if (params) { + const param = params.find(p => p.name === value.data[0]); + + if (param) { + result = param.value; + break; + } + } + + result = yield* this._query(scope, ...value.data); break; case 'enter': @@ -172,17 +193,17 @@ class Collector { result = []; for (const val of value) { - result.push(yield* this._collect(group, val, scope)); + result.push(yield* this._collect(group, val, scope, params)); } } else { assert(isNode(value)); - result = yield* this._collect(group, value, scope); + result = yield* this._collect(group, value, scope, params); } } } - * _query(scope, name) { - let result = scope.query(name); + * _query(scope, name, params) { + let result = scope.query(name, params); // TODO: warning. assert.notEqual(result.type, 'unknown'); @@ -190,6 +211,9 @@ class Collector { // Resulting scope is always the best choice for waiting. scope = result.scope; + // It's only valid the sequence: E*[CT]?F, + // where E - external, C - declaration, T - template, F - definition. + switch (result.type) { case 'external': const modulePath = scope.resolve(result.info.path); @@ -199,7 +223,7 @@ class Collector { const module = this.modules.get(modulePath); const {imported} = result.info; - while ((result = module.query(imported)).type === 'unknown') { + while ((result = module.query(imported, params)).type === 'unknown') { yield; } @@ -208,16 +232,27 @@ class Collector { } // TODO: reexports. - assert.equal(result.type, 'declaration'); + assert(result.type === 'declaration' || result.type === 'template'); scope = result.scope; name = result.name; // Fallthrough. case 'declaration': - this._freestyle(extractors.definition, result.node, scope); + case 'template': + let tmplParams = null; - while ((result = scope.query(name)).type === 'declaration') { + if (result.type === 'template') { + tmplParams = result.params.map((p, i) => ({ + name: p.name, + value: params[i] || p.default, + })); + } + + this._freestyle(extractors.definition, result.node, scope, tmplParams); + + while ((result = scope.query(name, params)).type !== 'definition') { + assert.notEqual(result.type, 'external'); yield; } @@ -231,7 +266,7 @@ class Collector { * _grabExports(module) { for (const [scope, name] of module.exports()) { - yield* this._query(scope, name); + yield* this._query(scope, name, null); } } @@ -268,10 +303,6 @@ class Collector { } } -function isNode(it) { - return it && typeof it === 'object' && it.type; -} - function pathToNamespace(path) { const pathObj = pathlib.parse(path); @@ -288,4 +319,15 @@ function isAcceptableGroup(group, node) { return group.entries.includes(node.type); } +function generateGenericName(base, params) { + let name = base + '_'; + + for (const {value} of params) { + assert.equal(typeof value, 'string'); + name += '_' + value; + } + + return name; +} + module.exports = Collector; diff --git a/lib/commands.js b/lib/commands.js index 58e36f2..1994123 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -1,8 +1,8 @@ 'use strict'; class Command { - static declare(name, node) { - return new Command('declare', [name, node]); + static declare(name, node, params) { + return new Command('declare', [name, node, params]); } static define(schema, declared = true) { @@ -17,8 +17,8 @@ class Command { return new Command('provide', [name, reference]); } - static query(name) { - return new Command('query', name); + static query(name, params = null) { + return new Command('query', [name, params]); } static enter() { diff --git a/lib/extractors.js b/lib/extractors.js index f2477bc..bd089d3 100644 --- a/lib/extractors.js +++ b/lib/extractors.js @@ -3,7 +3,7 @@ const assert = require('assert'); const {declare, define, external, provide, query, enter, exit, namespace} = require('./commands'); -const {partition} = require('./utils'); +const {partition, isNode} = require('./utils'); const definition = { entries: [ @@ -199,8 +199,13 @@ const definition = { * GenericTypeAnnotation(node) { const name = yield node.id; + const params = node.typeParameters && (yield node.typeParameters); - const schema = yield query(name); + const schema = yield query(name, params); + + if (typeof schema === 'string') { + return schema; + } if (schema.$unwrap) { return schema.type; @@ -215,6 +220,10 @@ const definition = { return makeFullname(schema); }, + * TypeParameterInstantiation(node) { + return yield node.params; + }, + * FunctionTypeAnnotation(node) { return null; }, @@ -385,32 +394,46 @@ const declaration = { * TypeAlias(node) { const name = yield node.id; + const params = node.typeParameters && (yield node.typeParameters); - yield declare(name, node); + yield declare(name, node, params); return name; }, * InterfaceDeclaration(node) { const name = yield node.id; + const params = node.typeParameters && (yield node.typeParameters); - yield declare(name, node); + yield declare(name, node, params); return name; }, * ClassDeclaration(node) { const name = yield node.id; + const params = node.typeParameters && (yield node.typeParameters); // TODO: do it only for "all"-mode. const body = node.body; yield body.body.filter(is('ClassMethod')); - yield declare(name, node); + yield declare(name, node, params); return name; }, + * TypeParameterDeclaration(node) { + return yield node.params; + }, + + * TypeParameter(node) { + return { + name: node.name, + default: node.default ? yield node.default : null, + }; + }, + /* * Utility. */ diff --git a/lib/module.js b/lib/module.js index 61e0c3b..858fc23 100644 --- a/lib/module.js +++ b/lib/module.js @@ -20,7 +20,7 @@ class Module { this._exports.set(name, [scope, reference]); } - query(name) { + query(name, params) { const result = this._exports.get(name); if (!result) { @@ -31,7 +31,7 @@ class Module { const [scope, reference] = result; - return scope.query(reference); + return scope.query(reference, params); } resolve(path) { diff --git a/lib/scope.js b/lib/scope.js index 68d02b5..76a4a2a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -37,22 +37,43 @@ class Scope { return new Scope(this, module || this.module); } - addDeclaration(name, node) { + addDeclaration(name, node, params) { assert(!this.entries.has(name)); - this.entries.set(name, { - type: 'declaration', + const isTemplate = Boolean(params); + + const entry = { + type: isTemplate ? 'template' : 'declaration', name, node, scope: this, - }); + }; + + if (isTemplate) { + entry.params = params; + entry.instances = []; + } + + this.entries.set(name, entry); + } + + addInstance(name, schema, params) { + const template = this.entries.get(name); + + assert(template); + assert.equal(template.type, 'template'); + + template.instances.push({params, schema}); } addDefinition(schema, declared) { + const decl = this.entries.get(schema.name); + if (declared) { - assert.equal((this.entries.get(schema.name) || {}).type, 'declaration'); + assert(decl); + assert.equal(decl.type, 'declaration'); } else { - assert(!this.entries.has(schema.name)); + assert(!decl); } this.entries.set(schema.name, { @@ -84,11 +105,26 @@ class Scope { return this.module.resolve(path); } - query(name) { - const result = this.entries.get(name); + query(name, params) { + const entry = this.entries.get(name); - if (result) { - return result; + if (entry && entry.type === 'template') { + assert(params); + + const augmented = entry.params.map((p, i) => params[i] || p.default); + const schema = findInstance(entry, augmented); + + if (schema) { + return { + type: 'definition', + schema, + scope: entry.scope, + }; + } + } + + if (entry) { + return entry; } if (this.parent) { @@ -101,4 +137,17 @@ class Scope { } } +function findInstance(template, queried) { + for (const {schema, params} of template.instances) { + // TODO: compare complex structures. + const same = params.every((p, i) => p === queried[i]); + + if (same) { + return schema; + } + } + + return null; +} + module.exports = Scope; diff --git a/lib/utils.js b/lib/utils.js index 6fc116c..04260b1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -11,6 +11,11 @@ function partition(iter, predicate) { return [left, right]; } +function isNode(it) { + return it && typeof it === 'object' && it.type; +} + module.exports = { partition, + isNode, }; diff --git a/tests/generics.js b/tests/generics.js new file mode 100644 index 0000000..76a97f6 --- /dev/null +++ b/tests/generics.js @@ -0,0 +1,10 @@ +type A = { + t: T, + k: K, +}; + +type X = { + a: A, +}; + +export {X}; diff --git a/tests/generics.json b/tests/generics.json new file mode 100644 index 0000000..833536f --- /dev/null +++ b/tests/generics.json @@ -0,0 +1,20 @@ +{ + "schemas": [ + { + "type": "record", + "name": "A__string_double", + "namespace": "generics", + "fields": [ + {"name": "t", "type": "string"}, + {"name": "k", "type": "double"} + ] + }, + { + "type": "record", + "name": "X", + "namespace": "generics", + "fields": [{"name": "a", "type": "A__string_double"}] + } + ], + "taskCount": 4 +}