UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

396 lines 16.8 kB
"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