diff --git a/lib/collector.js b/lib/collector.js index 19c29c1..d13590d 100644 --- a/lib/collector.js +++ b/lib/collector.js @@ -19,11 +19,12 @@ class Collector { this.taskCount = 0; this.active = true; this.modules = new Map; + this.roots = new Set; this.global = Scope.global(globals); this.running = false; } - collect(path) { + collect(path, internal = false) { // TODO: follow symlinks. path = pathlib.resolve(path); @@ -44,22 +45,30 @@ class Collector { const scope = this.global.extend(module); - this._spawn(ast.program, scope); + this._freestyle(extractors.declaration, ast.program, scope); this.modules.set(path, module); - if (!this.running) { - try { - this.running = true; + if (this.running) { + return; + } + + try { + this.running = true; + this._schedule(); + + if (!internal) { + const task = this._grabExports(module); + this._spawn(task); this._schedule(); - } finally { - this.running = false; } + } finally { + this.running = false; } } // Given the AST output of babylon parse, walk through in a depth-first order. - _spawn(root, scope) { + _freestyle(group, root, scope) { let stack; let parent; let keys = []; @@ -79,20 +88,17 @@ class Collector { const node = parent ? parent[keys[index]] : root; - if (isNode(node)) { - const {type} = node; - - const group = type && getExtractorsGroup(type); - - if (group) { + if (isNode(node) && isAcceptableGroup(group, node)) { + if (!this.roots.has(node)) { const task = this._collect(group, node, scope); - - this.tasks.add(task); - ++this.taskCount; - - continue; + this.roots.add(node); + this._spawn(task); } + continue; + } + + if (isNode(node) || node instanceof Array) { stack = { parent, keys, index, prev: stack }; parent = node; keys = Object.keys(node); @@ -105,7 +111,7 @@ class Collector { const extractor = group[node.type]; if (!extractor) { - assert.fail(`No extractor for "${node.type}" in group "${group.entries[0]}"`); + this._freestyle(group, node, scope); return null; } @@ -126,13 +132,13 @@ class Collector { if (value instanceof Command) { switch (value.name) { - case 'spawn': - this._spawn(value.data, scope); + case 'declare': + scope.addDeclaration(value.data[0], value.data[1]); break; case 'define': - scope.addSchema(value.data); - this.schemas.push(value.data); + scope.addDefinition(value.data[0], value.data[1]); + this.schemas.push(value.data[0]); break; case 'external': @@ -140,7 +146,7 @@ class Collector { break; case 'provide': - scope.addExport(value.data); + scope.addExport(value.data[0], value.data[1]); break; case 'query': @@ -175,34 +181,62 @@ class Collector { } * _query(scope, name) { - let result; + let result = scope.query(name); - // Wait any information about the reference. - while ((result = scope.query(name)).type === 'unknown') { - yield; + // TODO: warning. + assert.notEqual(result.type, 'unknown'); + + // Resulting scope is always the best choice for waiting. + scope = result.scope; + + switch (result.type) { + case 'external': + const modulePath = scope.resolve(result.info.path); + + this.collect(modulePath, true); + + const module = this.modules.get(modulePath); + const {imported} = result.info; + + while ((result = module.query(imported)).type === 'unknown') { + yield; + } + + if (result.type === 'definition') { + return result.schema; + } + + // TODO: reexports. + assert.equal(result.type, 'declaration'); + + scope = result.scope; + name = result.name; + + // Fallthrough. + case 'declaration': + this._freestyle(extractors.definition, result.node, scope); + + while ((result = scope.query(name)).type === 'declaration') { + yield; + } + + assert.equal(result.type, 'definition'); + + // Fallthrough. + case 'definition': + return result.schema; } + } - if (result.type === 'local') { - return result.schema; + * _grabExports(module) { + for (const [scope, name] of module.exports.values()) { + yield* this._query(scope, name); } + } - assert.equal(result.type, 'external'); - - // TODO: reexports. - - const modulePath = scope.resolve(result.info.path); - - this.collect(modulePath); - - const module = this.modules.get(modulePath); - const {imported} = result.info; - let schema; - - while (!(schema = module.query(imported))) { - yield; - } - - return schema; + _spawn(task) { + this.tasks.add(task); + ++this.taskCount; } _schedule() { @@ -234,7 +268,7 @@ class Collector { } function isNode(it) { - return it && typeof it === 'object' && (it.type || it.length); + return it && typeof it === 'object' && it.type; } function pathToNamespace(path) { @@ -249,8 +283,8 @@ function pathToNamespace(path) { .join('.'); } -function getExtractorsGroup(type) { - return extractors.find(group => group.entries.includes(type)); +function isAcceptableGroup(group, node) { + return group.entries.includes(node.type); } module.exports = Collector; diff --git a/lib/commands.js b/lib/commands.js index 316800e..16fb855 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -1,20 +1,20 @@ 'use strict'; class Command { - static spawn(node) { - return new Command('spawn', node); + static declare(name, node) { + return new Command('declare', [name, node]); } - static define(schema) { - return new Command('define', schema); + static define(schema, declared = true) { + return new Command('define', [schema, declared]); } static external(external) { return new Command('external', external); } - static provide(internal) { - return new Command('provide', internal); + static provide(name, reference) { + return new Command('provide', [name, reference]); } static query(name) { diff --git a/lib/extractors.js b/lib/extractors.js index aa8aa2b..0e26655 100644 --- a/lib/extractors.js +++ b/lib/extractors.js @@ -2,11 +2,15 @@ const assert = require('assert'); -const {spawn, define, external, provide, query, enter, exit, namespace} = require('./commands'); +const {declare, define, external, provide, query, enter, exit, namespace} = require('./commands'); const {partition} = require('./utils'); -const typedefsGroup = { - entries: ['TypeAlias', 'InterfaceDeclaration', 'ClassDeclaration'], +const definition = { + entries: [ + 'TypeAlias', + 'InterfaceDeclaration', + 'ClassDeclaration', + ], * TypeAlias(node) { let schema = yield node.right; @@ -83,9 +87,6 @@ const typedefsGroup = { }, * ClassMethod(node) { - // For cases, when the body has references to the class. - yield spawn(node.body); - return null; }, @@ -235,25 +236,39 @@ const typedefsGroup = { }, }; -/* - * TODO: declarations. - */ -const blocksGroup = { - entries: ['BlockStatement'], +const declaration = { + entries: [ + // Blocks. + 'Program', + 'BlockStatement', + // Imports. + 'ImportDeclaration', + 'VariableDeclarator', + // Exports. + 'ExportNamedDeclaration', + 'ExportDefaultDeclaration', + ], + + /* + * Blocks. + */ + + * Program(node) { + yield node.body; + }, * BlockStatement(node) { yield enter(); - yield spawn(node.body); + yield node.body; yield exit(); }, -}; -/* - * TODO: warning about "import typeof". - * TODO: support form "import *". - */ -const importsGroup = { - entries: ['ImportDeclaration', 'VariableDeclarator'], + /* + * Imports. + * + * TODO: warning about "import typeof". + * TODO: support form "import *". + */ * ImportDeclaration(node) { const specifiers = yield node.specifiers; @@ -325,6 +340,81 @@ const importsGroup = { }; }, + /* + * Exports. + * + * TODO: support "export from" form. + * TODO: support commonjs. + */ + + * ExportDefaultDeclaration(node) { + const reference = yield node.declaration; + + if (reference) { + yield provide(null, reference); + } + }, + + * ExportNamedDeclaration(node) { + if (!node.declaration) { + yield node.specifiers; + return; + } + + const reference = yield node.declaration; + + if (reference) { + yield provide(reference, reference); + } + }, + + * ExportSpecifier(node) { + const reference = yield node.local; + let name = yield node.exported; + + if (name === 'default') { + name = null; + } + + yield provide(name, reference); + }, + + /* + * Declarations. + */ + + * TypeAlias(node) { + const name = yield node.id; + + yield declare(name, node); + + return name; + }, + + * InterfaceDeclaration(node) { + const name = yield node.id; + + yield declare(name, node); + + return name; + }, + + * ClassDeclaration(node) { + const name = yield node.id; + + // TODO: do it only for "all"-mode. + const body = node.body; + yield body.body.filter(is('ClassMethod')); + + yield declare(name, node); + + return name; + }, + + /* + * Utility. + */ + * StringLiteral(node) { return node.value; }, @@ -334,64 +424,6 @@ const importsGroup = { }, }; -/* - * TODO: support "export from" form. - * TODO: support commonjs. - */ -const exportsGroup = Object.assign({}, typedefsGroup, { - entries: ['ExportNamedDeclaration', 'ExportDefaultDeclaration'], - - * ExportDefaultDeclaration(node) { - let schema = yield node.declaration; - - if (!schema) { - return; - } - - if (typeof schema === 'string') { - schema = yield query(schema); - } - - yield provide({ - schema, - exported: null, - }); - }, - * ExportNamedDeclaration(node) { - if (!node.declaration) { - yield node.specifiers; - return; - } - - const schema = yield node.declaration; - - if (!schema) { - return; - } - - yield provide({ - schema, - exported: schema.name, - }); - }, - - * ExportSpecifier(node) { - let exported = yield node.exported; - - if (exported === 'default') { - exported = null; - } - - const name = yield node.local; - const schema = yield query(name); - - yield provide({ - schema, - exported, - }); - }, -}); - function* extractLastPragma(comments) { const pragmas = (yield comments).filter(Boolean); @@ -415,7 +447,7 @@ function* extractProperty(prop, value) { if (type.type === 'record') { type.namespace = yield namespace(); - yield define(type); + yield define(type, false); type = type.name; } @@ -556,9 +588,11 @@ function mergeSchemas(schemas) { }; } -module.exports = [ - typedefsGroup, - blocksGroup, - importsGroup, - exportsGroup, -]; +function is(type) { + return node => Boolean(node) && node.type === type; +} + +module.exports = { + definition, + declaration, +}; diff --git a/lib/scope.js b/lib/scope.js index 9c4dce5..7705d76 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -10,7 +10,7 @@ class Scope { const global = new Scope(null, null); for (const schema of schemas) { - global.addSchema(schema); + global.addDefinition(schema, false); } return global; @@ -20,8 +20,7 @@ class Scope { this.parent = parent; this.module = module; this.scopeId = module && module.generateScopeId(); - this.schemas = new Map; - this.imports = new Map; + this.entries = new Map; } get namespace() { @@ -41,22 +40,45 @@ class Scope { return new Scope(this, module || this.module); } - addSchema(schema) { - assert(!this.schemas.has(schema.name)); + addDeclaration(name, node) { + assert(!this.entries.has(name)); - this.schemas.set(schema.name, schema); + this.entries.set(name, { + type: 'declaration', + name, + node, + scope: this, + }); + } + + addDefinition(schema, declared = true) { + if (declared) { + assert.equal((this.entries.get(schema.name) || {}).type, 'declaration'); + } else { + assert(!this.entries.has(schema.name)); + } + + this.entries.set(schema.name, { + type: 'definition', + schema, + scope: this, + }); } addImport(info) { - assert(!this.imports.has(info.local)); + assert(!this.entries.has(info.local)); - this.imports.set(info.local, info); + this.entries.set(info.local, { + type: 'external', + info, + scope: this, + }); } - addExport(info) { + addExport(name, reference) { assert(this.module); - this.module.addExport(info.exported, info.schema); + this.module.addExport(name, this, reference); } resolve(path) { @@ -66,7 +88,7 @@ class Scope { } query(name) { - const result = this._querySchema(name) || this._queryImport(name); + const result = this.entries.get(name); if (result) { return result; @@ -80,24 +102,6 @@ class Scope { type: 'unknown', }; } - - _querySchema(name) { - const schema = this.schemas.get(name); - - return schema && { - type: 'local', - schema, - }; - } - - _queryImport(name) { - const info = this.imports.get(name); - - return info && { - type: 'external', - info, - }; - } } class Module { @@ -112,12 +116,22 @@ class Module { return this.scopeCount++; } - addExport(name, schema) { - this.exports.set(name, schema); + addExport(name, scope, reference) { + this.exports.set(name, [scope, reference]); } query(name) { - return this.exports.get(name) || null; + const result = this.exports.get(name); + + if (!result) { + return { + type: 'unknown', + }; + } + + const [scope, reference] = result; + + return scope.query(reference); } resolve(path) { diff --git a/tests/arrays.js b/tests/arrays.js index d0028d6..c505a7e 100644 --- a/tests/arrays.js +++ b/tests/arrays.js @@ -9,3 +9,5 @@ interface Interface { class Class { a: number[]; } + +export {Type, Interface, Class}; diff --git a/tests/arrays.json b/tests/arrays.json index b5ee7da..ae5c693 100644 --- a/tests/arrays.json +++ b/tests/arrays.json @@ -28,5 +28,5 @@ }] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/disorder.js b/tests/disorder.js index 5b1d76d..1d91772 100644 --- a/tests/disorder.js +++ b/tests/disorder.js @@ -7,3 +7,5 @@ type Y = { }; type Z = string; + +export {X}; diff --git a/tests/disorder.json b/tests/disorder.json index 1f8f3d8..8f094a1 100644 --- a/tests/disorder.json +++ b/tests/disorder.json @@ -18,5 +18,5 @@ "fields": [{"name": "y", "type": "Y"}] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/enums.js b/tests/enums.js index 474b68f..095cfdf 100644 --- a/tests/enums.js +++ b/tests/enums.js @@ -9,3 +9,5 @@ interface Interface { class Class { a: 'one' | 'two'; } + +export {Type, Interface, Class}; diff --git a/tests/enums.json b/tests/enums.json index 7c4e34a..88f53b9 100644 --- a/tests/enums.json +++ b/tests/enums.json @@ -28,5 +28,5 @@ }] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/externals.js b/tests/externals.js index 74a97d0..ec338f1 100644 --- a/tests/externals.js +++ b/tests/externals.js @@ -21,3 +21,5 @@ type Y = { k: T, p: P, }; + +export {X, Y}; diff --git a/tests/externals.json b/tests/externals.json index e18b4dd..746ea04 100644 --- a/tests/externals.json +++ b/tests/externals.json @@ -71,5 +71,5 @@ ] } ], - "taskCount": 18 + "taskCount": 17 } diff --git a/tests/externals/second.js b/tests/externals/second.js index 9c80c40..bf98733 100644 --- a/tests/externals/second.js +++ b/tests/externals/second.js @@ -1,8 +1,8 @@ -export type N = { +type N = { n: boolean, }; -class M { +export class M { m: string; } @@ -10,8 +10,8 @@ interface KK { k: number; } -class P { +export default class P { p: number; } -export {M, KK as K, P as default}; +export {N, KK as K}; diff --git a/tests/inheritance.js b/tests/inheritance.js index 435e610..11de023 100644 --- a/tests/inheritance.js +++ b/tests/inheritance.js @@ -21,3 +21,5 @@ interface Y extends X { interface Z extends Y { z: boolean; } + +export {C, Z}; diff --git a/tests/inheritance.json b/tests/inheritance.json index 0200d47..2553257 100644 --- a/tests/inheritance.json +++ b/tests/inheritance.json @@ -51,5 +51,5 @@ ] } ], - "taskCount": 6 + "taskCount": 8 } diff --git a/tests/intersections.js b/tests/intersections.js index 8c16949..9d59064 100644 --- a/tests/intersections.js +++ b/tests/intersections.js @@ -11,3 +11,5 @@ type Y = { class Z { z: A & C; } + +export {X, Y, Z}; diff --git a/tests/intersections.json b/tests/intersections.json index 9c11e51..61a3bc7 100644 --- a/tests/intersections.json +++ b/tests/intersections.json @@ -12,12 +12,6 @@ "namespace": "intersections", "fields": [{"name": "b", "type": "string"}] }, - { - "type": "record", - "name": "C", - "namespace": "intersections", - "fields": [{"name": "c", "type": "boolean"}] - }, { "type": "record", "name": "X", @@ -27,6 +21,12 @@ {"name": "b", "type": "string"} ] }, + { + "type": "record", + "name": "C", + "namespace": "intersections", + "fields": [{"name": "c", "type": "boolean"}] + }, { "type": "record", "name": "_A_B_C", @@ -63,5 +63,5 @@ ] } ], - "taskCount": 6 + "taskCount": 8 } diff --git a/tests/maps.js b/tests/maps.js index 30fbe81..c94b8a1 100644 --- a/tests/maps.js +++ b/tests/maps.js @@ -5,3 +5,5 @@ type Type = { interface Interface { [string]: number; } + +export {Type, Interface}; diff --git a/tests/maps.json b/tests/maps.json index d8c9d3c..4d2b574 100644 --- a/tests/maps.json +++ b/tests/maps.json @@ -13,5 +13,5 @@ "values": "double" } ], - "taskCount": 2 + "taskCount": 4 } diff --git a/tests/pragmas.js b/tests/pragmas.js index eb5d77c..da6ee23 100644 --- a/tests/pragmas.js +++ b/tests/pragmas.js @@ -36,3 +36,5 @@ class Class { /* $avro fixed 10 */ e: Buffer; } + +export {Type, Interface, Class}; diff --git a/tests/pragmas.json b/tests/pragmas.json index 0c5f6d7..61de5df 100644 --- a/tests/pragmas.json +++ b/tests/pragmas.json @@ -37,5 +37,5 @@ ] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/primitives.js b/tests/primitives.js index 3610bd4..a2f6823 100644 --- a/tests/primitives.js +++ b/tests/primitives.js @@ -21,3 +21,5 @@ class Class { d: null; e: Buffer; } + +export {Type, Interface, Class}; diff --git a/tests/primitives.json b/tests/primitives.json index ebe42c5..fe77b49 100644 --- a/tests/primitives.json +++ b/tests/primitives.json @@ -37,5 +37,5 @@ ] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/references.js b/tests/references.js index 2628cef..8c9413c 100644 --- a/tests/references.js +++ b/tests/references.js @@ -17,3 +17,5 @@ class Class { b: A[]; c: A; } + +export {Type, Interface, Class}; diff --git a/tests/references.json b/tests/references.json index f7e77f7..15b096e 100644 --- a/tests/references.json +++ b/tests/references.json @@ -36,5 +36,5 @@ ] } ], - "taskCount": 4 + "taskCount": 6 } diff --git a/tests/scopes.js b/tests/scopes.js index 96bed2c..8952f45 100644 --- a/tests/scopes.js +++ b/tests/scopes.js @@ -20,6 +20,9 @@ type Y = { x: X, z: Z, }; + + // TODO: replace with commonjs. + export {Y as Y2}; } class Test { @@ -30,6 +33,8 @@ type Y = { x: X, z: Z, }; + + export {Y as Y3}; } baz() { @@ -39,6 +44,12 @@ type Y = { x: X, z: Z, }; + + export {Y as Y4}; } } + + export {Y as Y1}; })(); + +export {Y as Y0}; diff --git a/tests/scopes.json b/tests/scopes.json index 1cf42e9..c448374 100644 --- a/tests/scopes.json +++ b/tests/scopes.json @@ -22,22 +22,16 @@ "namespace": "scopes._1", "fields": [{"name": "x", "type": "X"}] }, - { - "type": "string", - "name": "Z", - "namespace": "scopes._1" - }, - { - "type": "record", - "name": "Test", - "namespace": "scopes._1", - "fields": [] - }, { "type": "boolean", "name": "X", "namespace": "scopes._2" }, + { + "type": "string", + "name": "Z", + "namespace": "scopes._1" + }, { "type": "record", "name": "Y", @@ -76,5 +70,5 @@ ] } ], - "taskCount": 16 + "taskCount": 17 } diff --git a/tests/shadowing.js b/tests/shadowing.js index adae167..642abe7 100644 --- a/tests/shadowing.js +++ b/tests/shadowing.js @@ -8,4 +8,9 @@ type X = { type Y = { y: Buffer, }; + + // TODO: replace with commonjs. + export {Y}; })(); + +export {X}; diff --git a/tests/shadowing.json b/tests/shadowing.json index b7fc183..b5d8598 100644 --- a/tests/shadowing.json +++ b/tests/shadowing.json @@ -23,5 +23,5 @@ ] } ], - "taskCount": 4 + "taskCount": 6 } diff --git a/tests/skipFunctions.js b/tests/skipFunctions.js index feda1de..cf91c84 100644 --- a/tests/skipFunctions.js +++ b/tests/skipFunctions.js @@ -29,3 +29,5 @@ class Class { baz: () => void; } + +export {Type, Interface, Class}; diff --git a/tests/skipFunctions.json b/tests/skipFunctions.json index 1c9c0c9..5047765 100644 --- a/tests/skipFunctions.json +++ b/tests/skipFunctions.json @@ -28,5 +28,5 @@ ] } ], - "taskCount": 6 + "taskCount": 8 } diff --git a/tests/typeInMethod.js b/tests/typeInMethod.js index dad0468..5d1b378 100644 --- a/tests/typeInMethod.js +++ b/tests/typeInMethod.js @@ -3,5 +3,10 @@ class Test { type X = { t: Test, }; + + // TODO: replace with commonjs. + export {X}; } } + +export {Test}; diff --git a/tests/typeInMethod.json b/tests/typeInMethod.json index ecf4032..a048b22 100644 --- a/tests/typeInMethod.json +++ b/tests/typeInMethod.json @@ -13,5 +13,5 @@ "fields": [{"name": "t", "type": "typeInMethod.Test"}] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/unions.js b/tests/unions.js index e913ee6..e7b0b54 100644 --- a/tests/unions.js +++ b/tests/unions.js @@ -12,3 +12,5 @@ class Class { a: string | number; b: ?string; } + +export {Type, Interface, Class}; diff --git a/tests/unions.json b/tests/unions.json index 0b36643..0142faf 100644 --- a/tests/unions.json +++ b/tests/unions.json @@ -46,5 +46,5 @@ ] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/unionsAndEnums.js b/tests/unionsAndEnums.js index fece477..160f44c 100644 --- a/tests/unionsAndEnums.js +++ b/tests/unionsAndEnums.js @@ -9,3 +9,5 @@ interface Interface { class Class { a: 'one' | 'two' | number; } + +export {Type, Interface, Class}; diff --git a/tests/unionsAndEnums.json b/tests/unionsAndEnums.json index e112f50..798a879 100644 --- a/tests/unionsAndEnums.json +++ b/tests/unionsAndEnums.json @@ -37,5 +37,5 @@ }] } ], - "taskCount": 3 + "taskCount": 5 } diff --git a/tests/valueAsType.js b/tests/valueAsType.js index 4cba863..dc30ad3 100644 --- a/tests/valueAsType.js +++ b/tests/valueAsType.js @@ -9,3 +9,5 @@ interface Interface { class Class { a: 'one'; } + +export {Type, Interface, Class}; diff --git a/tests/valueAsType.json b/tests/valueAsType.json index a351744..81c752c 100644 --- a/tests/valueAsType.json +++ b/tests/valueAsType.json @@ -25,5 +25,5 @@ ] } ], - "taskCount": 3 + "taskCount": 5 }