js-slang
Version:
Javascript-based implementations of Source, written in Typescript
396 lines • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getProgramNames = exports.getKeywords = exports.DeclarationKind = void 0;
const lodash_1 = require("lodash");
const constants_1 = require("../constants");
const finder_1 = require("../finder");
const loader_1 = require("../modules/loader");
const utils_1 = require("../modules/utils");
const syntax_1 = require("../parser/source/syntax");
const helpers_1 = require("../utils/ast/helpers");
const typeGuards_1 = require("../utils/ast/typeGuards");
var DeclarationKind;
(function (DeclarationKind) {
DeclarationKind["KIND_IMPORT"] = "import";
DeclarationKind["KIND_FUNCTION"] = "func";
DeclarationKind["KIND_LET"] = "let";
DeclarationKind["KIND_PARAM"] = "param";
DeclarationKind["KIND_CONST"] = "const";
DeclarationKind["KIND_KEYWORD"] = "keyword";
})(DeclarationKind = exports.DeclarationKind || (exports.DeclarationKind = {}));
function isFunction(node) {
return (node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression');
}
function isLoop(node) {
return node.type === 'WhileStatement' || node.type === 'ForStatement';
}
// Update this to use exported check from "acorn-loose" package when it is released
function isDummyName(name) {
return name === '✖';
}
const KEYWORD_SCORE = 20000;
// Ensure that keywords are prioritized over names
const keywordsInBlock = {
FunctionDeclaration: [
{ name: 'function', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }
],
VariableDeclaration: [
{ name: 'const', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }
],
AssignmentExpression: [{ name: 'let', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }],
WhileStatement: [{ name: 'while', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }],
IfStatement: [
{ name: 'if', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE },
{ name: 'else', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }
],
ForStatement: [{ name: 'for', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }]
};
const keywordsInLoop = {
BreakStatement: [{ name: 'break', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }],
ContinueStatement: [
{ name: 'continue', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }
]
};
const keywordsInFunction = {
ReturnStatement: [{ name: 'return', meta: DeclarationKind.KIND_KEYWORD, score: KEYWORD_SCORE }]
};
/**
* Retrieves keyword suggestions based on what node the cursor is currently over.
* For example, only suggest `let` when the cursor is over the init part of a for
* statement
* @param prog Program to parse
* @param cursorLoc Current location of the cursor
* @param context Evaluation context
* @returns A list of keywords as suggestions
*/
function getKeywords(prog, cursorLoc, context) {
const identifier = (0, finder_1.findIdentifierNode)(prog, context, cursorLoc);
if (!identifier) {
return [];
}
const ancestors = (0, finder_1.findAncestors)(prog, identifier);
if (!ancestors) {
return [];
}
// In the init part of a for statement, `let` is the only valid keyword
if (ancestors[0].type === 'ForStatement' && identifier === ancestors[0].init) {
return context.chapter >= syntax_1.default.AssignmentExpression
? keywordsInBlock.AssignmentExpression
: [];
}
const keywordSuggestions = [];
function addAllowedKeywords(keywords) {
Object.entries(keywords)
.filter(([nodeType]) => context.chapter >= syntax_1.default[nodeType])
.forEach(([_nodeType, decl]) => keywordSuggestions.push(...decl));
}
// The rest of the keywords are only valid at the beginning of a statement
if (ancestors[0].type === 'ExpressionStatement' &&
(ancestors[0].loc ?? constants_1.UNKNOWN_LOCATION).start === (identifier.loc ?? constants_1.UNKNOWN_LOCATION).start) {
addAllowedKeywords(keywordsInBlock);
// Keywords only allowed in functions
if (ancestors.some(node => isFunction(node))) {
addAllowedKeywords(keywordsInFunction);
}
// Keywords only allowed in loops
if (ancestors.some(node => isLoop(node))) {
addAllowedKeywords(keywordsInLoop);
}
}
return keywordSuggestions;
}
exports.getKeywords = getKeywords;
/**
* Retrieve the list of names present within the program. If the cursor is within a comment,
* or when the user is declaring a variable or function arguments, suggestions should not be displayed,
* indicated by the second part of the return value of this function.
* @param prog Program to parse for names
* @param comments Comments found within the program
* @param cursorLoc Current location of the cursor
* @returns Tuple consisting of the list of suggestions, and a boolean value indicating if
* suggestions should be displayed, i.e. `[suggestions, shouldPrompt]`
*/
async function getProgramNames(prog, comments, cursorLoc) {
function before(first, second) {
return first.line < second.line || (first.line === second.line && first.column <= second.column);
}
function cursorInLoc(nodeLoc) {
if (nodeLoc === null || nodeLoc === undefined) {
return false;
}
return before(nodeLoc.start, cursorLoc) && before(cursorLoc, nodeLoc.end);
}
for (const comment of comments) {
if (cursorInLoc(comment.loc)) {
// User is typing comments
return [[], false];
}
}
// BFS to get names
const queue = [prog];
const nameQueue = [];
while (queue.length > 0) {
// Workaround due to minification problem
// tslint:disable-next-line
const node = queue.shift();
if (isFunction(node)) {
// This is the only time we want raw identifiers
nameQueue.push(...node.params);
}
const body = getNodeChildren(node);
for (const child of body) {
if ((0, typeGuards_1.isImportDeclaration)(child)) {
nameQueue.push(child);
}
if ((0, typeGuards_1.isDeclaration)(child)) {
nameQueue.push(child);
}
if (cursorInLoc(child.loc)) {
queue.push(child);
}
}
}
// Do not prompt user if he is declaring a variable
for (const nameNode of nameQueue) {
if (cursorInIdentifier(nameNode, n => cursorInLoc(n.loc))) {
return [[], false];
}
}
// This implementation is order dependent, so we can't
// use something like Promise.all
const res = {};
let idx = 0;
for (const node of nameQueue) {
const names = await getNames(node, n => cursorInLoc(n.loc));
names.forEach(decl => {
// Deduplicate, ensure deeper declarations overwrite
res[decl.name] = { ...decl, score: idx };
idx++;
});
}
return [Object.values(res), true];
}
exports.getProgramNames = getProgramNames;
function isNotNull(x) {
// This function exists to appease the mighty typescript type checker
return x !== null;
}
function isNotNullOrUndefined(x) {
// This function also exists to appease the mighty typescript type checker
return x !== undefined && isNotNull(x);
}
function getNodeChildren(node) {
switch (node.type) {
case 'Program':
return node.body;
case 'BlockStatement':
return node.body;
case 'WhileStatement':
return [node.test, node.body];
case 'ForStatement':
return [node.init, node.test, node.update, node.body].filter(isNotNullOrUndefined);
case 'ExpressionStatement':
return [node.expression];
case 'IfStatement':
const children = [node.test, node.consequent];
if (isNotNullOrUndefined(node.alternate)) {
children.push(node.alternate);
}
return children;
case 'ReturnStatement':
return node.argument ? [node.argument] : [];
case 'FunctionDeclaration':
return [node.body];
case 'VariableDeclaration':
return node.declarations.flatMap(getNodeChildren);
case 'VariableDeclarator':
return node.init ? [node.init] : [];
case 'ArrowFunctionExpression':
return [node.body];
case 'FunctionExpression':
return [node.body];
case 'UnaryExpression':
return [node.argument];
case 'BinaryExpression':
return [node.left, node.right];
case 'LogicalExpression':
return [node.left, node.right];
case 'ConditionalExpression':
return [node.test, node.alternate, node.consequent];
case 'CallExpression':
return [...node.arguments, node.callee];
// case 'Identifier':
// case 'DebuggerStatement':
// case 'BreakStatement':
// case 'ContinueStatement':
// case 'MemberPattern':
case 'ArrayExpression':
return node.elements.filter(isNotNull);
case 'AssignmentExpression':
return [node.left, node.right];
case 'MemberExpression':
return [node.object, node.property];
case 'Property':
return [node.key, node.value];
case 'ObjectExpression':
return [...node.properties];
case 'NewExpression':
return [...node.arguments, node.callee];
default:
return [];
}
}
function cursorInIdentifier(node, locTest) {
switch (node.type) {
case 'VariableDeclaration':
for (const decl of node.declarations) {
if (locTest(decl.id)) {
return true;
}
}
return false;
case 'FunctionDeclaration':
return node.id ? locTest(node.id) : false;
case 'Identifier':
return locTest(node);
default:
return false;
}
}
function docsToHtml(spec, obj) {
const importedName = (0, helpers_1.getImportedName)(spec);
const nameStr = importedName === spec.local.name ? '' : `Imported as ${spec.local.name}\n`;
switch (obj.kind) {
case 'function': {
let paramStr;
if (obj.params.length === 0) {
paramStr = '()';
}
else {
paramStr = `(${obj.params.map(([name, type]) => `${name}: ${type}`).join(', ')})`;
}
const header = `${importedName}${paramStr} → {${obj.retType}}`;
return `<div><h4>${header}</h4><div class="description">${nameStr}${obj.description}</div></div>`;
}
case 'variable':
return `<div><h4>${importedName}: ${obj.type}</h4><div class="description">${nameStr}${obj.description}</div></div>`;
case 'unknown':
return `<div><h4>${importedName}: unknown</h4><div class="description">${nameStr}No description available</div></div>`;
}
}
// locTest is a callback that returns whether cursor is in location of node
/**
* Gets a list of `NameDeclarations` from the given node
* @param node Node to search for names
* @param locTest Callback of type `(node: Node) => boolean`. Should return true if the cursor
* is located within the node, false otherwise
* @returns List of found names
*/
async function getNames(node, locTest) {
switch (node.type) {
case 'ImportDeclaration':
const specs = node.specifiers.filter(x => !isDummyName(x.local.name));
const moduleName = (0, helpers_1.getModuleDeclarationSource)(node);
// Don't try to load documentation for local modules
if (!(0, utils_1.isSourceModule)(moduleName)) {
return specs.map(spec => {
if (spec.type === 'ImportNamespaceSpecifier') {
return {
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Namespace import of '${moduleName}'`
};
}
return {
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Import '${(0, helpers_1.getImportedName)(spec)}' from '${moduleName}'`
};
});
}
try {
const [namespaceSpecs, otherSpecs] = (0, lodash_1.partition)(specs, typeGuards_1.isNamespaceSpecifier);
const manifest = await (0, loader_1.memoizedGetModuleManifestAsync)();
if (!(moduleName in manifest)) {
// Unknown module
const namespaceDecls = namespaceSpecs.map(spec => ({
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Namespace import of unknown module '${moduleName}'`
}));
const otherDecls = otherSpecs.map(spec => ({
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Import from unknown module '${moduleName}'`
}));
return namespaceDecls.concat(otherDecls);
}
const namespaceDecls = namespaceSpecs.map(spec => ({
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Namespace import of '${moduleName}'`
}));
// If there are only namespace specifiers, then don't bother
// loading the documentation
if (otherSpecs.length === 0)
return namespaceDecls;
const docs = await (0, loader_1.memoizedGetModuleDocsAsync)(moduleName, true);
return namespaceDecls.concat(otherSpecs.map(spec => {
const importedName = (0, helpers_1.getImportedName)(spec);
if (docs[importedName] === undefined) {
return {
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `No documentation available for <code>${importedName}</code> from '${moduleName}'`
};
}
else {
return {
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: docsToHtml(spec, docs[importedName])
};
}
}));
}
catch (err) {
// Failed to load docs for whatever reason
return specs.map(spec => ({
name: spec.local.name,
meta: DeclarationKind.KIND_IMPORT,
docHTML: `Unable to retrieve documentation for '${moduleName}'`
}));
}
case 'VariableDeclaration':
const declarations = [];
for (const decl of node.declarations) {
const id = decl.id;
const name = id.name;
if (!name ||
isDummyName(name) ||
(decl.init && !isFunction(decl.init) && locTest(decl.init)) // Avoid suggesting `let foo = foo`, but suggest recursion with arrow functions
) {
continue;
}
if (node.kind === DeclarationKind.KIND_CONST && decl.init && isFunction(decl.init)) {
// constant initialized with arrow function will always be a function
declarations.push({ name, meta: DeclarationKind.KIND_FUNCTION });
}
else {
declarations.push({ name, meta: node.kind });
}
}
return declarations;
case 'FunctionDeclaration':
return node.id && !isDummyName(node.id.name)
? [{ name: node.id.name, meta: DeclarationKind.KIND_FUNCTION }]
: [];
case 'Identifier': // Function/Arrow function param
return !isDummyName(node.name) ? [{ name: node.name, meta: DeclarationKind.KIND_PARAM }] : [];
default:
return [];
}
}
//# sourceMappingURL=index.js.map