js-slang
Version:
Javascript-based implementations of Source, written in Typescript
158 lines • 6.62 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultLinkerOptions = void 0;
const parser_1 = require("../../parser/parser");
const errors_1 = require("../errors");
const helpers_1 = require("../../utils/ast/helpers");
const misc_1 = require("../../utils/misc");
const utils_1 = require("../../parser/utils");
const typeGuards_1 = require("../../utils/ast/typeGuards");
const directedGraph_1 = require("./directedGraph");
const resolver_1 = require("./resolver");
/**
* Helper error type. Thrown to cause any Promise.all calls
* to reject immediately instead of just returning undefined,
* which would still require all promises to be resolved
*/
class LinkerError extends Error {
}
exports.defaultLinkerOptions = {
resolverOptions: resolver_1.defaultResolutionOptions
};
/**
* Starting from the entrypoint file, parse all imported local modules and create
* a dependency graph.
*
* @param fileGetter A function that, when given a file path, either returns the contents
* of that file as a string, or if it doesn't exist, `undefined`
*/
async function parseProgramsAndConstructImportGraph(fileGetter, entrypointFilePath, context, options = exports.defaultLinkerOptions, shouldAddFileName) {
const importGraph = new directedGraph_1.DirectedGraph();
const programs = {};
const files = {};
const sourceModulesToImport = new Set();
// Wrapper around resolve file to make calling it more convenient
async function resolveDependency(fromPath, node) {
const toPath = (0, helpers_1.getModuleDeclarationSource)(node);
const resolveResult = await (0, resolver_1.default)(fromPath, toPath, fileGetter, options.resolverOptions);
if (!resolveResult) {
throw new errors_1.ModuleNotFoundError(toPath, node);
}
if (resolveResult.type === 'source') {
// We can assume two things:
// 1. Source modules do not depend on one another
// 2. They will always be loaded first before any local modules
// Thus it is not necessary to track them in the import graph
sourceModulesToImport.add(toPath);
return;
}
const { absPath, contents } = resolveResult;
// Special case of circular import: the module specifier
// refers to the current file
if (absPath === fromPath) {
throw new errors_1.CircularImportError([absPath, absPath]);
}
node.source.value = absPath;
importGraph.addEdge(absPath, fromPath);
// No need to parse programs we've already parsed before
if (absPath in programs)
return;
await parseAndEnumerateModuleDeclarations(absPath, contents);
}
async function parseAndEnumerateModuleDeclarations(fromModule, fileText) {
const parseOptions = shouldAddFileName
? {
sourceFile: fromModule
}
: {};
const program = (0, parser_1.parse)(fileText, context, parseOptions);
if (!program) {
// The program has syntax errors or something,
// exit early
throw new LinkerError();
}
programs[fromModule] = program;
files[fromModule] = fileText;
await Promise.all((0, misc_1.mapAndFilter)(program.body, node => {
switch (node.type) {
case 'ExportNamedDeclaration': {
if (!node.source)
return undefined;
// case falls through!
}
case 'ImportDeclaration':
case 'ExportAllDeclaration':
// We still have to visit each node to update
// each node's source value
return resolveDependency(fromModule, node);
default:
return undefined;
}
}));
}
let entrypointFileText = undefined;
function hasVerboseErrors() {
// Always try to infer if verbose errors should be enabled
let statement = null;
if (!programs[entrypointFilePath]) {
if (entrypointFileText === undefined) {
// non-existent entrypoint
return false;
}
// There are syntax errors in the entrypoint file
// we use parseAt to try parse the first line
statement = (0, utils_1.parseAt)(entrypointFileText, 0);
}
else {
// Otherwise we can use the entrypoint program as it has been passed
const entrypointProgram = programs[entrypointFilePath];
// Check if the program had any code at all
if (entrypointProgram.body.length === 0)
return false;
[statement] = entrypointProgram.body;
}
if (statement === null)
return false;
// The two different parsers end up with two different ASTs
// These are the two cases where 'enable verbose' appears
// as a directive
if ((0, typeGuards_1.isDirective)(statement)) {
return statement.directive === 'enable verbose';
}
return statement.type === 'Literal' && statement.value === 'enable verbose';
}
try {
entrypointFileText = await fileGetter(entrypointFilePath);
// Not using boolean test here, empty strings are valid programs
// but are falsy
if (entrypointFileText === undefined) {
throw new errors_1.ModuleNotFoundError(entrypointFilePath);
}
await parseAndEnumerateModuleDeclarations(entrypointFilePath, entrypointFileText);
const topologicalOrderResult = importGraph.getTopologicalOrder();
if (topologicalOrderResult.isValidTopologicalOrderFound) {
return {
ok: true,
topoOrder: topologicalOrderResult.topologicalOrder,
programs,
sourceModulesToImport,
files,
verboseErrors: hasVerboseErrors()
};
}
context.errors.push(new errors_1.CircularImportError(topologicalOrderResult.firstCycleFound));
}
catch (error) {
if (!(error instanceof LinkerError)) {
// Any other error that occurs is just appended to the context
// and we return undefined
context.errors.push(error);
}
}
return {
ok: false,
verboseErrors: hasVerboseErrors()
};
}
exports.default = parseProgramsAndConstructImportGraph;
//# sourceMappingURL=linker.js.map
;