184 lines
5.0 KiB
JavaScript
184 lines
5.0 KiB
JavaScript
// @flow
|
|
|
|
import * as fs from 'fs';
|
|
import * as pathlib from 'path';
|
|
import wu from 'wu';
|
|
import {isNode} from '@babel/types';
|
|
import type {Node} from '@babel/types';
|
|
|
|
import traverse from './traverse';
|
|
import globals from './globals';
|
|
import definitionGroup from './definitions';
|
|
import declarationGroup from './declarations';
|
|
import Fund from '../fund';
|
|
import Module from './module';
|
|
import Scope from './scope';
|
|
import Context from './context';
|
|
import {invariant} from '../utils';
|
|
import type Parser from '../parser';
|
|
import type {Type, TypeId} from '../types';
|
|
import type {TemplateParam} from './query';
|
|
|
|
const VISITOR = Object.assign({}, definitionGroup, declarationGroup);
|
|
|
|
export default class Collector {
|
|
+root: string;
|
|
+parser: Parser;
|
|
_fund: Fund;
|
|
_modules: Map<string, Module>;
|
|
_global: Scope;
|
|
|
|
constructor(parser: Parser, root: string = '.') {
|
|
this.root = root;
|
|
this.parser = parser;
|
|
this._fund = new Fund;
|
|
this._modules = new Map;
|
|
this._global = Scope.global(globals);
|
|
}
|
|
|
|
collect(path: string, internal: boolean = false) {
|
|
// TODO: follow symlinks.
|
|
path = pathlib.resolve(path);
|
|
|
|
let module = this._modules.get(path);
|
|
|
|
if (module) {
|
|
return;
|
|
}
|
|
|
|
// TODO: error wrapping.
|
|
const code = fs.readFileSync(path, 'utf8');
|
|
const ast = this.parser.parse(code);
|
|
|
|
// TODO: customize it.
|
|
// XXX: replace with normal resolver and path-to-id converter.
|
|
const id = pathToId(path.replace(/source\.js$/, ''));
|
|
module = new Module(id, path);
|
|
|
|
const scope = this._global.extend(module);
|
|
|
|
this._freestyle(ast.program, scope, []);
|
|
|
|
this._modules.set(path, module);
|
|
|
|
if (!internal) {
|
|
this._grabExports(module);
|
|
}
|
|
}
|
|
|
|
finish(): Fund {
|
|
return this._fund;
|
|
}
|
|
|
|
_freestyle(root: Node, scope: Scope, params: TemplateParam[]) {
|
|
const ctx = new Context(this, scope, params);
|
|
|
|
const iter = traverse(root);
|
|
let result = iter.next();
|
|
|
|
while (!result.done) {
|
|
const node = result.value;
|
|
const detain = node.type in VISITOR;
|
|
|
|
if (detain) {
|
|
VISITOR[node.type](ctx, node);
|
|
}
|
|
|
|
result = iter.next(detain);
|
|
}
|
|
}
|
|
|
|
_query(scope: Scope, name: string, params: (?Type)[]): ?Type {
|
|
let result = scope.query(name, params);
|
|
|
|
// TODO: warning.
|
|
invariant(result.kind !== 'unknown');
|
|
|
|
if (result.kind !== 'special') {
|
|
// 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.kind) {
|
|
case 'external':
|
|
const modulePath = scope.resolve(result.info.path);
|
|
|
|
this.collect(modulePath, true);
|
|
|
|
const module = this._modules.get(modulePath);
|
|
|
|
invariant(module);
|
|
|
|
const {imported} = result.info;
|
|
|
|
result = module.query(imported, params);
|
|
|
|
if (result.kind === 'definition') {
|
|
return result.type;
|
|
}
|
|
|
|
// TODO: reexports.
|
|
invariant(result.kind === 'declaration' || result.kind === 'template');
|
|
|
|
scope = result.scope;
|
|
name = result.name;
|
|
|
|
// Fallthrough.
|
|
case 'declaration':
|
|
case 'template':
|
|
const tmplParams = [];
|
|
|
|
if (result.kind === 'template') {
|
|
for (const [i, p] of result.params.entries()) {
|
|
tmplParams.push({
|
|
name: p.name,
|
|
value: params[i] === undefined ? p.value : params[i],
|
|
});
|
|
}
|
|
}
|
|
|
|
invariant(result.kind === 'declaration' || result.kind === 'template');
|
|
|
|
this._freestyle(result.node, scope, tmplParams);
|
|
|
|
result = scope.query(name, params);
|
|
|
|
invariant(result.kind === 'definition');
|
|
|
|
// Fallthrough.
|
|
case 'definition':
|
|
return result.type;
|
|
|
|
case 'special':
|
|
default:
|
|
const resolve = id => this._fund.take(id);
|
|
return result.call(params, resolve);
|
|
}
|
|
}
|
|
|
|
_grabExports(module: Module) {
|
|
for (const [scope, name] of module.exports()) {
|
|
const type = this._query(scope, name, []);
|
|
|
|
if (type) {
|
|
this._fund.put(type, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function pathToId(path: string): TypeId {
|
|
const relPath = pathlib.relative('.', path);
|
|
const pathObj = pathlib.parse(relPath);
|
|
|
|
return pathlib.format({
|
|
dir: pathObj.dir,
|
|
name: pathObj.name,
|
|
})
|
|
// TODO: replace invalid chars.
|
|
.split(pathlib.sep);
|
|
}
|