UNPKG

equicord-companion

Version:

Equicord Companion is a vscode extension to test Equicord patches & webpack finds right from the comfort of your IDE

1,420 lines (1,144 loc) 46.8 kB
/* eslint-disable @stylistic/indent-binary-ops */ import { AstParser } from "@ast/AstParser"; import { allEntries, Cache, CacheGetter, findObjectLiteralByKey, findParent, findReturnIdentifier, findReturnPropertyAccessExpression, getLeadingIdentifier, isSyntaxList, lastParent, zeroRange, } from "@ast/util"; import { outputChannel } from "@extension"; import { ModuleCache, ModuleDepManager } from "@modules/cache"; import { format } from "@modules/format"; import { formatModule } from "@modules/util"; import { sendAndGetData } from "@server/index"; import { Definitions, ExportMap, ExportRange, ModuleDeps, RangeExportMap, RawExportMap, RawExportRange, References, Store, } from "@type/ast"; import { isAccessorDeclaration } from "tsutils"; import { VariableInfo } from "tsutils/util/usage"; import { CallExpression, createSourceFile, Expression, Identifier, isArrowFunction, isBinaryExpression, isBlock, isCallExpression, isClassDeclaration, isConstructorDeclaration, isExpressionStatement, isFunctionExpression, isIdentifier, isMethodDeclaration, isNewExpression, isNumericLiteral, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSpreadAssignment, isVariableDeclaration, MemberName, NewExpression, Node, ObjectLiteralExpression, PropertyAccessExpression, ScriptKind, ScriptTarget, SourceFile, } from "typescript"; import { Location, Position, Range } from "vscode"; // FIXME: rewrite to use module cache export class WebpackAstParser extends AstParser { /** * This is set on {@link RangeExportMap} when the default export is commonjs and has no properties, eg, string literal, function */ static readonly SYM_CJS_DEFAULT: unique symbol = Symbol.for("CommonJS Default Export"); /** * * @param text the module text * @returns a {@link WebpackAstParser} * * NOTE: you probably want {@link WebpackAstParser.withFormattedModule|withFormattedModule} */ public static override withFormattedText(text: string): WebpackAstParser { return new this(format(text)); } public static withFormattedModule(text: string, moduleId: string | number): WebpackAstParser { return new this(format(formatModule(text, moduleId))); } /** * The webpack instance passed to this module * * The `n` of * ``` * function (e, t, n) { // webpack module contents * } * ``` */ @CacheGetter() get wreq(): Identifier | undefined { return this.findWebpackArg(); } /** where {@link WebpackAstParser.wreq this.wreq} is used*/ @CacheGetter() get uses(): VariableInfo | undefined { return this.wreq && this.vars.get(this.wreq); } /** * The module id of the current module */ @CacheGetter() get moduleId(): string | null { if (this.text.startsWith("// Webpack Module ")) { const [, id] = this.text.match(/^\/\/ Webpack Module (\d+) /) ?? []; return id || null; } return null; } public constructor(text: string) { super(text); } protected override createSourceFile(): SourceFile { return createSourceFile("module.js", this.text, ScriptTarget.ESNext, true, ScriptKind.JS); } /** * @param paramIndex the index of the param 0, 1, 2 etc... * @param start finds a webpack arg from the source tree * @returns the identifier of the param if found or undef */ findWebpackArg(paramIndex = 2, start: Node = this.sourceFile): Identifier | undefined { for (const n of start.getChildren()) { if (isSyntaxList(n) || isExpressionStatement(n) || isBinaryExpression(n)) return this.findWebpackArg(paramIndex, n); if (isFunctionExpression(n)) { if (n.parameters.length > 3 || n.parameters.length < paramIndex + 1) return; const p = n.parameters[paramIndex].name; if (!p) return; if (!isIdentifier(p)) return; return p; } } } public async getModulesThatRequireThisModule(): Promise<ModuleDeps | null> { if (!this.moduleId) { return null; } if (!ModuleDepManager.hasModDeps()) { await ModuleDepManager.initModDeps({ fromDisk: true }); } const guh = ModuleDepManager.getModDeps(this.moduleId); return { lazy: guh.lazyUses, sync: guh.syncUses, }; } @Cache() public getModulesThatThisModuleRequires(): ModuleDeps | null { if (!this.wreq || !this.uses) return null; // flatmaps because .map(...).filter(x => x !== false) isn't a valid typeguard /** * things like wreq(moduleid) */ const wreqCalls = this.uses.uses .map((x) => x.location) .flatMap((v) => { const p = findParent(v, isCallExpression); if (!p || p.expression !== v) return []; if (p.arguments.length === 1 && isNumericLiteral(p.arguments[0])) return p.arguments[0].text; return []; }); const lazyModules = this.uses.uses .map((x) => x.location) .flatMap((v) => { const [, prop] = getLeadingIdentifier(v); if (prop?.text !== "bind") return []; const call = findParent(v, isCallExpression); if (!call) return []; if (call.arguments.length === 2 && isNumericLiteral(call.arguments[1])) return call.arguments[1].text; return []; }); return { lazy: lazyModules, sync: wreqCalls, }; } public async generateDefinitions(position: Position): Definitions { if (!this.uses) throw new Error("Wreq isn't used anywhere"); // map the assignment of required modules to their uses const modules = new Map([...this.vars.entries()].filter(([k]) => { return this.uses!.uses.some((e) => { const node = findParent(e.location, isVariableDeclaration); return node?.name === k; }); })); const x = this.getTokenAtOffset(this.offsetAt(position)); const accessChain = findParent(x, isPropertyAccessExpression); if (!accessChain) return; const importChain = this.flattenPropertyAccessExpression(accessChain); if (!importChain) return; let [requiredModule, ...names] = importChain; if (!requiredModule) return; const [, dec] = [...modules.entries()].find(([, v]) => { return v.uses.some(({ location }) => requiredModule === location); }) ?? []; const moduleId = this.getModuleId(dec); if (!moduleId) return; const res = await sendAndGetData<"rawId">({ type: "rawId", data: { id: moduleId, }, }) .catch(outputChannel.error); if (res?.data == null) return; let cur = WebpackAstParser.withFormattedModule(res.data, moduleId); if (names.length < 1) { return { range: zeroRange, uri: cur.mkStringUri(), }; } while (true) { const ret = cur.doesReExportFromExport(names.map((x) => x.text)); if (!ret) { break; } const [importSourceId] = ret; [, names] = ret; const res = await sendAndGetData<"rawId">({ type: "rawId", data: { id: +importSourceId, }, }) .catch(outputChannel.error); if (!res?.data) { outputChannel.error("Failed to get data from client"); return; } cur = WebpackAstParser.withFormattedModule(res.data, importSourceId); } const maybeRange: Range = cur .findExportLocation(names.map((x) => x.text)); return { range: maybeRange, uri: cur.mkStringUri(), }; // const maybeRange = new WebpackAstParser(res.data) // .findExportLocation(names.map((x) => x.text)); // if (maybeRange instanceof Range) { // return { // range: maybeRange, // uri: mkStringUri(res.data), // }; // } // return { // range: // names.length >= 1 // ? new WebpackAstParser(res.data) // .findExportLocation(names.map((x) => x.text)) // : zeroRange, // uri: mkStringUri(res.data), // }; } doesReExportFromExport(exportName: (string | symbol)[]): [importSourceId: string, exportName: MemberName[]] | undefined { const map = this.getExportMapRaw(); const exp = this.getNestedExportFromMap(exportName, map); const last = exp?.at(-1); if (!last) return; // TODO: handle more cases than just property access if (!isPropertyAccessExpression(last)) return; const [imported, ...chain] = this.flattenPropertyAccessExpression(last) ?? []; if (!this.isIdentifier(imported) || chain.length === 0) return; const importedId = this.getIdOfImportedVar(imported); if (!importedId) return; return [importedId, chain]; } /** * gets the module id from a require * given * ```js * var mod = n(123456); * ``` * @argument dec the variable info for that mod * @see {@link getVariableInitializer} which can than be passed into {@link vars|vars.get} * @returns `123456` */ getModuleId(dec: VariableInfo | undefined): number | undefined { if (!dec) return undefined; if (dec.declarations.length !== 1) return undefined; const init = findParent(dec.declarations[0], isVariableDeclaration)?.initializer; if (!init || !isCallExpression(init)) return undefined; if (init.arguments.length !== 1 || !isNumericLiteral(init.arguments[0])) return undefined; const num = +init.arguments[0].text; return num; } public async generateReferences(position: Position): References { if (!this.moduleId) throw new Error("Could not find module id of module to search for references of"); if (!ModuleDepManager.hasModDeps()) { await ModuleDepManager.initModDeps({ fromDisk: true }); } const moduleExports = this.getExportMap(); const where = await this.getModulesThatRequireThisModule(); const locs: Location[] = []; const exportedNames = Object.entries(moduleExports) .filter(([, v]) => Array.isArray(v) && v.some((x) => { if (!x) return; return x.contains(position); })); // TODO: support jumping from object literals for (const [_exportedName] of exportedNames) { // needed to workaround a v8 bug which crashes when a breakpoint falls on the for loop // see: https://github.com/microsoft/vscode/issues/246902 const exportedName = _exportedName; const seen: Record<string, Set<String>> = {}; // the module id that is being searched for uses // the ID of the module that exportName will be imported from // the exported name to search type ElementType = [ moduleId: string, importedId: string, exportName: string | symbol, ]; const left: ElementType[] = where?.sync.map((x) => [x, this.moduleId!, exportedName] as const) ?? []; let cur: ElementType | undefined; while ((cur = left.pop())) { const [modId, importedId, exportedName] = cur; if (seen[importedId]?.has(modId)) { continue; } (seen[importedId] ||= new Set()).add(modId); const modText = await ModuleCache.getModuleFromNum(modId); if (!modText) continue; const parser = new WebpackAstParser(modText); const uses = parser.getUsesOfImport(importedId, exportedName); // FIXME: this covers up a bug in {@link doesReExport} if (uses.length === 0) continue; const exportedAs = parser.doesReExportFromImport(importedId, exportedName); if (exportedAs) { const where = await parser.getModulesThatRequireThisModule(); left.push(...where?.sync.map((x) => [x, parser.moduleId!, exportedAs] as ElementType) ?? []); } locs.push(...uses.map((x) => new Location(ModuleCache.getModuleURI(modId), x))); } } return locs; } /** * @param moduleId the module id that {@link exportName} is from * @param exportName the name of the re-exported export */ public doesReExportFromImport( moduleId: string, exportName: string | symbol, ): string | symbol | undefined { // we can't re-export anything if we don't import anything if (!this.wreq || !this.moduleId) return; const decl = this.getImportedVar(moduleId); if (!decl) return; // FIXME: handle re-exports as cjs default, Object.entries ignores symbols const maybeReExports = Object.entries(this.getExportMapRaw()) .filter(([, _v]) => { // FIXME: properly handle this with {@link exportName} if (!Array.isArray(_v)) return false; const [v] = _v; if (isIdentifier(v)) { return this.isUseOf(v, decl); } else if (isPropertyAccessExpression(v)) { const [module, reExport] = getLeadingIdentifier(v); if (!module) return false; // you cant discriminate against destructured unions return this.isUseOf(module, decl) && reExport!.text === exportName; } outputChannel.warn(`Unhandled type for reExport: ${v.kind}`); return false; }) .map(([k]) => k); if (maybeReExports.length !== 1) { if (maybeReExports.length > 1) { throw new Error(`Found more than one reExport for wreq(${moduleId}).${String(exportName)} in ${this.moduleId}`); } return; } return maybeReExports[0]; } // TODO: add tests for this func /** * @returns a map of exported names to the nodes that they are exported from */ @Cache() getExportMapRaw(): RawExportMap { return { ...this.getExportMapRawWreq_d() ?? {}, ...this.getExportMapRawWreq_t() ?? {}, ...this.getExportMapRawWreq_e() ?? {}, }; } @Cache() public getExportMapRawWreq_d(): | ExportMap<Identifier | PropertyAccessExpression> | undefined { const wreqD = this.findWreq_d(); if (!wreqD) return; const [, exports] = wreqD.arguments; return Object.fromEntries(exports.properties .map((x) => { if ( !isPropertyAssignment(x) || !( isArrowFunction(x.initializer) || isFunctionExpression(x.initializer) ) ) return false; const ret = findReturnIdentifier(x.initializer) ?? findReturnPropertyAccessExpression(x.initializer); return ret != null && [x.name.getText(), [ret]]; }) .filter((x) => x !== false)); } @Cache() public getExportMapRawWreq_e(): ExportMap<Expression> | undefined { const wreqE = this.findWreq_e(); if (!wreqE) return; const uses = this.vars.get(wreqE); if (!uses) return; const exportAssignments = uses.uses .filter(({ location }) => { const [, moduleProp] = getLeadingIdentifier(location); return moduleProp?.text === "exports"; }) .map((x) => x.location) .map((x) => { let name: string | symbol | undefined = this.flattenPropertyAccessExpression(lastParent(x, isPropertyAccessExpression))?.[2]?.text; name ||= WebpackAstParser.SYM_CJS_DEFAULT; const ret = findParent(x, isBinaryExpression)?.right; return ret && [name, [ret]]; }) .filter((x) => x !== undefined); if (exportAssignments.length === 0) return; return Object.fromEntries(exportAssignments); } @Cache() public getExportMapRawWreq_t(): ExportMap<Expression> | undefined { const wreqT = this.findWreq_t(); if (!wreqT) return; const uses = this.vars.get(wreqT); if (!uses) return; return Object.fromEntries(uses.uses .map(({ location }): [string, Expression[]] | undefined => { const [, exportAssignment] = getLeadingIdentifier(location); const binary = findParent(location, isBinaryExpression); if (exportAssignment && binary?.right) { return [exportAssignment.text, [binary.right]]; } return undefined; }) .filter((x) => x !== undefined)); } @Cache() getExportMap(): RangeExportMap { return { ...this.getExportMapWreq_d() ?? {}, ...this.getExportMapWreq_t() ?? {}, ...this.getExportMapWreq_e() ?? {}, }; } public getImportedVar(moduleId: string): Identifier | undefined { if (!this.wreq) throw new Error("Wreq is not used in this file"); const uses = this.uses!.uses.find(({ location }) => { const call = findParent(location, isCallExpression); return ( call?.arguments.length === 1 && call.arguments[0].getText() === moduleId ); }); const ret = findParent(uses?.location, isVariableDeclaration)?.name; if (this.isIdentifier(ret)) return ret; } // TODO: trace over ```js // var foo = wreq(1) // , used = n.n(foo); // TODO: add tests public getIdOfImportedVar(variable: Identifier): string | undefined { const uses = (this.getVarInfoFromUse(variable) ?? this.vars.get(variable))?.declarations[0]; if (!uses) return; const decl = findParent(uses, isVariableDeclaration); if (!decl) return; const initExpr = decl.initializer; if (!initExpr || !isCallExpression(initExpr)) return; const [id] = initExpr.arguments; if (!this.isLiteralish(id)) { outputChannel.warn("id is not literalish"); return; } return id.getText(); } /** * @param moduleId the imported module id where {@link exportName} is used * @param exportName the string of the exported name or {@link SYM_CJS_DEFAULT} for the default export * TODO: support nested exports eg: `wreq(123).ZP.storeMethod()` * @returns the ranges where the export is used in this file */ getUsesOfImport(moduleId: string, exportName: string | symbol): Range[] { if (!this.wreq) throw new Error("Wreq is not used in this file"); if (typeof exportName === "symbol" && exportName !== WebpackAstParser.SYM_CJS_DEFAULT) { throw new Error("Invalid symbol for exportName"); } const uses: Range[] = []; for (const { location } of this.vars.get(this.wreq)?.uses ?? []) { if (!isCallExpression(location.parent)) continue; if (location.parent.arguments[0].getText() !== moduleId) continue; const norm = location?.parent?.parent; if (norm && isVariableDeclaration(norm)) { if (!isIdentifier(norm.name)) continue; const importUses = this.vars.get(norm.name); // handle things like `var foo = wreq(1), bar = wreq.n(foo)` nmd: { if (importUses?.uses.length === 1) { const loc = importUses.uses[0].location; const call = findParent(loc, isCallExpression); if ( !call || call.arguments.length !== 1 || call.arguments[0] !== loc ) break nmd; // ensure the call is `n.n(...)` const funcExpr = call.expression; // ensure something like `foo.bar` if ( !isPropertyAccessExpression(funcExpr) || !isIdentifier(funcExpr.name) || !isIdentifier(funcExpr.expression) ) break nmd; // ensure the first part is wreq if ( !this.isUseOf(funcExpr.expression, this.wreq) || funcExpr.name.text !== "n" ) break nmd; const decl = findParent(funcExpr, isVariableDeclaration)?.name; if (!decl || !isIdentifier(decl)) break nmd; this.vars .get(decl) ?.uses?.map((x) => x.location.parent) .filter(isCallExpression) .map((calledUse): Range[] | undefined => { if (exportName === WebpackAstParser.SYM_CJS_DEFAULT) { // TODO: handle default exports other than just functions return isCallExpression(calledUse.parent) ? [this.makeRangeFromAstNode(calledUse)] : undefined; } else if (typeof exportName === "string") { const expr = findParent( calledUse, isPropertyAccessExpression, ); if ( !( !!expr && expr.expression === calledUse && expr.name.text === exportName ) ) return undefined; return [this.makeRangeFromAstNode(expr.name)]; } throw new Error("Invalid exportName"); }) .filter((x) => x !== undefined) .forEach((use) => { const final = use.at(-1); if (!final) throw new Error("Final is undefined, this should have been filtered out by the previous line as there should be no empty arrays"); uses.push(final); }); } } for (const { location } of importUses?.uses ?? []) { if (!isPropertyAccessExpression(location.parent)) continue; if (!isIdentifier(location.parent.name)) continue; if (location.parent.name.getText() !== exportName) continue; uses.push(this.makeRangeFromAstNode(location.parent.name)); } continue; } const direct = location.parent; if (isCallExpression(direct)) { if (!isPropertyAccessExpression(direct.parent)) continue; if (!isIdentifier(direct.parent.name)) continue; if (direct.parent.name.text !== exportName) continue; uses.push(this.makeRangeFromAstNode(direct.parent.name)); } } return uses; } @Cache() getExportMapWreq_t(): RangeExportMap | undefined { const wreqT = this.findWreq_t(); if (!wreqT) return undefined; const uses = this.vars.get(wreqT); if (!uses) return undefined; return Object.fromEntries(uses.uses .map(({ location }): [string, RangeExportMap[string]] | false => { const [, exportAssignment] = getLeadingIdentifier(location); const binary = findParent(location, isBinaryExpression); if (exportAssignment && binary && isIdentifier(binary?.right)) { return [ exportAssignment.text, [ this.makeRangeFromAstNode(exportAssignment), this.makeRangeFromAstNode(binary.right), this.makeRangeFromFunctionDef(binary.right), ].filter((x) => !!x), ]; } return exportAssignment ? [ exportAssignment.text, [this.makeRangeFromAstNode(exportAssignment)], ] : false; }) .filter((x) => x !== false) as any); } rawMakeExportMapRecursive(node: Node | undefined): RawExportMap | RawExportRange { if (!node) throw new Error("node should not be undefined"); if (isObjectLiteralExpression(node)) { const props = node.properties .map((x): false | [string | symbol, RawExportMap[PropertyKey]][] => { if (isSpreadAssignment(x)) { if (!isIdentifier(x.expression)) { outputChannel.error("Spread assignment is not an identifier, this should be handled"); } const spread = this.rawMakeExportMapRecursive(x.expression); if (Array.isArray(spread)) { outputChannel.warn("Identifier in object spread is not an object, this should be handled"); return false; } const { [WebpackAstParser.SYM_CJS_DEFAULT]: _default, ...rest } = spread; return Object.entries(rest); } return [[x.name.getText(), this.rawMakeExportMapRecursive(x)]]; }) .filter((x) => x !== false) .flat(); if (props.length !== 0) props.push([WebpackAstParser.SYM_CJS_DEFAULT, [node.getChildAt(0)]]); return Object.fromEntries(props); } else if (this.isLiteralish(node)) { return [node]; } else if (isPropertyAssignment(node)) { const objRange = this.rawMakeExportMapRecursive(node.initializer); if (Array.isArray(objRange)) return [node.name, ...[objRange].flat()]; return { [node.name.getText()]: objRange, }; } else if (this.isFunctionish(node)) { wrapperFuncCheck: { if (!node.body) break wrapperFuncCheck; // if the arrow function returns a simple identifier, use that if (isIdentifier(node.body) || isPropertyAccessExpression(node.body)) { const ret = this.rawMakeExportMapRecursive(node.body); if (allEntries(ret).length > 0) return ret; } if (isBlock(node.body) && node.body.statements.length === 1) { const ident = findReturnIdentifier(node); if (!ident) break wrapperFuncCheck; const ret = this.rawMakeExportMapRecursive(ident); if (allEntries(ret).length > 0) return ret; } } if (node.name) return [node.name]; return [node]; } else if (isCallExpression(node)) { return [node]; } else if (isIdentifier(node)) { const trail = this.unwrapVariableDeclaration(node); if (!trail || trail.length === 0) { outputChannel.warn("Could not find variable declaration for identifier"); return []; } const last = this.getVariableInitializer(trail.at(-1)!); if (!last) { outputChannel.warn("Could not find initializer of identifier"); return [trail.at(-1)!]; } return this.rawMakeExportMapRecursive(last); } return [node]; } rawMapToExportMap(map: RawExportRange): ExportRange; rawMapToExportMap(map: RawExportMap): RangeExportMap; rawMapToExportMap(map: RawExportMap | RawExportRange): RangeExportMap | ExportRange; rawMapToExportMap(map: RawExportMap | RawExportRange): RangeExportMap | ExportRange { if (Array.isArray(map)) { return map.map((node) => { if (this.isFunctionish(node) && !node.name) { return this.makeRangeFromAnonFunction(node); } return this.makeRangeFromAstNode(node); }); } return Object.fromEntries(allEntries(map) .map(([k, v]) => { return [k, this.rawMapToExportMap(v)]; })); } /** * takes an expression, and maps it to ranges which it is in */ makeExportMapRecursive(node: Node): RangeExportMap | ExportRange; makeExportMapRecursive(node: Node | undefined): RangeExportMap | ExportRange { if (!node) throw new Error("node should not be undefined / falsy"); return this.rawMapToExportMap(this.rawMakeExportMapRecursive(node)); } // FIXME: handle when there is more than one module.exports assignment, eg e = () => {}; e.foo = () => {}; @Cache() getExportMapWreq_e(): RangeExportMap | undefined { const wreqE = this.findWreq_e(); if (!wreqE) return undefined; const uses = this.vars.get(wreqE); if (!uses) return undefined; const exportAssignment = uses.uses.find(({ location }) => { const [, moduleProp] = getLeadingIdentifier(location); return moduleProp?.text === "exports"; }); if (!exportAssignment) return undefined; const exportObject = findParent( exportAssignment.location, isBinaryExpression, )?.right; if (!exportObject) { outputChannel.debug("Could not find export object"); return undefined; } const exports = this.makeExportMapRecursive(exportObject); if (Array.isArray(exports)) { return { [WebpackAstParser.SYM_CJS_DEFAULT]: exports, }; } return exports; } @Cache() getExportMapWreq_d(): RangeExportMap | undefined { const wreqD = this.findWreq_d(); if (!wreqD) return; const [, exports] = wreqD.arguments; return Object.fromEntries(exports.properties .map((x): false | [string, RangeExportMap[string]] => { if ( !isPropertyAssignment(x) || !( isArrowFunction(x.initializer) || isFunctionExpression(x.initializer) ) ) return false; let lastNode: Node | undefined = findReturnIdentifier(x.initializer); lastNode ??= findReturnPropertyAccessExpression(x.initializer); let ret: RangeExportMap | ExportRange | undefined = this.tryParseStoreForExport(lastNode, [this.makeRangeFromAstNode(x.name)]); ret ||= this.makeExportMapRecursive(x); // ensure we arent nested ret = (function nestLoop(curName: string | symbol, obj: RangeExportMap | ExportRange): RangeExportMap | ExportRange { if (Array.isArray(obj)) { return obj; } const keys = allEntries(obj); if (keys.length === 1) { if (obj[curName]) { return nestLoop(curName, obj[curName]); } const [[key]] = keys; obj[key] = nestLoop(key, obj[key]); return obj; } for (const [k] of keys) { obj[k] = nestLoop(k, obj[k]); } return obj; })(x.name.getText(), ret); return lastNode != null ? [x.name.getText(), ret] : false; }) .filter((x) => x !== false) as any); } tryParseStoreForExport( node: Node | undefined, extraStoreLocs: Range[] = [], ): RangeExportMap | undefined { if (!node) return; if (!isIdentifier(node)) { outputChannel.debug("Could not find identifier for store export"); return; } const decl = this.getVarInfoFromUse(node); if (!decl) return; const allUses = decl.uses .map(({ location }) => location) .concat(...decl.declarations); // find where it's set to the new store // there should never be more than one assignment const uses = allUses.filter((ident) => { return this.isVariableAssignmentLike(ident.parent); }); if (uses.length === 0) { return; } else if (uses.length > 1) { outputChannel.warn(`Found more than one store assignment in module ${this.moduleId}, this should not happen`); return; } const [use] = uses; const initializer = (() => { if (isVariableDeclaration(use.parent)) { if (!use.parent.initializer) { throw new Error("Variable declaration has no initializer, this should be filtered out by the previous isVariableAssignmentLike check"); } return use.parent.initializer; } else if (this.isAssignmentExpression(use.parent)) { return use.parent.right; } throw new Error("Unexpected type for use, this should not happen"); })(); if (!isNewExpression(initializer)) return; const store = this.tryParseStore(initializer); if (!store) { outputChannel.debug("Failed to parse store"); return; } const ret: RangeExportMap = {}; const def: Range[] = []; def.push(...extraStoreLocs); def.push(...store.store.map((x) => this.makeRangeFromAstNode(x))); ret[WebpackAstParser.SYM_CJS_DEFAULT] = def; for (const [name, loc] of allEntries(store.methods)) { const map = this.makeExportMapRecursive(loc); ret[name] = map; } for (const [name, loc] of allEntries(store.props)) { const map = this.makeExportMapRecursive(loc); ret[name] = map; } return ret; } // TODO: test this tryParseStore(storeInit: NewExpression): Store | undefined { const ret: Store = { store: [], fluxEvents: {}, methods: {}, props: {}, }; const storeVar = storeInit.expression; const args = storeInit.arguments; parseArgs: { if (!args) break parseArgs; if (args.length !== 2) { outputChannel.warn(`Incorrect number of arguments for a store instantiation, expected 2, found ${args?.length}`); break parseArgs; } const [, events] = args; if (!isObjectLiteralExpression(events)) { outputChannel.warn("Expected the flux events to be an object literal expression"); break parseArgs; } // FIXME: extract into function for (const prop of events.properties) { if (!isPropertyAssignment(prop)) { outputChannel.debug("found prob that is not a property assignment, this should be handled"); continue; } ret.fluxEvents[prop.name.getText()] = [prop.initializer]; if (isIdentifier(prop.initializer)) { const trail = this.unwrapVariableDeclaration(prop.initializer) ?.toReversed(); if (trail) ret.fluxEvents[prop.name.getText()].push(...trail); } } } if (!isIdentifier(storeVar)) { // TODO: parse this outputChannel.debug("anything than an identifier is not supported for store instantiations yet"); return; } ret.store.push(storeVar); const storeVarInfo = this.getVarInfoFromUse(storeVar); if (!storeVarInfo || storeVarInfo.declarations.length === 0) { outputChannel.debug("Could not find store declaration"); return; } if (storeVarInfo.declarations.length > 1) { outputChannel.warn("Found more than one store declaration, this should not happen"); return; } const [decl] = storeVarInfo.declarations; ret.store.push(decl); const classDecl = decl.parent; if (!isClassDeclaration(classDecl)) { outputChannel.warn("Store decl is not a class"); return; } // check if any of the extends clauses extend Store // this is the best we can do to ensure something is a store // TODO: make sure it does not extend a component const doesExtend = (classDecl.heritageClauses?.length ?? -1) > 0; if (!doesExtend) { outputChannel.debug("Store class does not extend Store"); return; } for (const member of classDecl.members) { if (isMethodDeclaration(member)) { if (!member.body) continue; ret.methods[member.name.getText()] = member; continue; } else if (isConstructorDeclaration(member)) { ret.store.push(member); } else if (isPropertyAssignment(member)) { ret.props[member.name.getText()] = member.initializer; } else if (isAccessorDeclaration(member)) { if (!member.body) continue; ret.methods[member.name.getText()] = member; } else { outputChannel.warn("Unhandled store member type. This should be handled"); } } return ret; } getNestedExportFromMap<T>(keys: readonly (string | symbol)[], map: ExportMap<T>): T[] | undefined { let i = 0; let cur: ExportMap<T>[keyof ExportMap<T>] = map; while ((cur = cur[keys[i++]])) { if (Array.isArray(cur)) { return cur; } else if (Array.isArray(cur[WebpackAstParser.SYM_CJS_DEFAULT])) { // @ts-expect-error i just fucking checked this typescript return cur[WebpackAstParser.SYM_CJS_DEFAULT]; } } return undefined; } findExportLocation(exportNames: readonly (string | symbol)[]): Range { let cur: RangeExportMap | ExportRange = this.getExportMap(); let range = zeroRange; let i = 0; while ((cur = cur[exportNames[i++]])) { if (Array.isArray(cur)) { const g = cur.at(-1); if (g) range = g; else outputChannel.error("Empty array of exports"); break; // fallback to the most appropriate thing // most of the time, it's the default export } else if (Array.isArray(cur[WebpackAstParser.SYM_CJS_DEFAULT])) { const g = (cur[WebpackAstParser.SYM_CJS_DEFAULT] as ExportRange).at(-1); if (g) range = g; else outputChannel.error("Empty array of exports"); } } return range; // return ( // this.tryFindExportwreq_d(exportName) // || this.tryFindExportWreq_t(exportName) // || this.tryFindExportsWreq_e(exportName) // || zeroRange // ); } @Cache() findWreq_d(): | (Omit<CallExpression, "arguments"> & { arguments: readonly [Identifier, ObjectLiteralExpression]; }) | undefined { if (this.uses) { const maybeWreqD = this.uses.uses.find((use) => getLeadingIdentifier(use.location)[1]?.text === "d")?.location.parent.parent; if (!maybeWreqD || !isCallExpression(maybeWreqD)) return undefined; if ( maybeWreqD.arguments.length !== 2 || !isIdentifier(maybeWreqD.arguments[0]) || !isObjectLiteralExpression(maybeWreqD.arguments[1]) ) return undefined; return maybeWreqD as any; } } tryFindExportwreq_d(exportName: string): Range | undefined { if (this.uses) { const wreq_dCall = this.findWreq_d(); if (!wreq_dCall) return undefined; // the a: function(){return b;} of wreq.d const exportCallAssignment = findObjectLiteralByKey( wreq_dCall.arguments[1], exportName, ); if ( !exportCallAssignment || !isPropertyAssignment(exportCallAssignment) || !( isFunctionExpression(exportCallAssignment.initializer) || isArrowFunction(exportCallAssignment.initializer) ) ) return undefined; const exportVar = findReturnIdentifier(exportCallAssignment.initializer); if (exportVar) { const [exportDec] = [...this.vars.entries()].find(([, v]) => { return v.uses.some((use) => use.location === exportVar); }) ?? []; if (!exportDec) return undefined; return this.makeRangeFromAstNode(exportDec); } const reExport = findReturnPropertyAccessExpression(exportCallAssignment.initializer); if (reExport) { return this.makeRangeFromAstNode(reExport.name); } } } @Cache() findWreq_t(): Identifier | undefined { return this.findWebpackArg(1); } tryFindExportWreq_t(exportName: string): Range | undefined { const wreq_t = this.findWreq_t(); if (!wreq_t) return undefined; const uses = this.vars.get(wreq_t); if (!uses) return undefined; const exports = uses.uses.find(({ location }) => { const [, exportAssignment] = getLeadingIdentifier(location); return exportAssignment?.text === exportName; }); return exports ? this.makeRangeFromAstNode(exports.location) : undefined; } findWreq_e(): Identifier | undefined { return this.findWebpackArg(0); } tryFindExportsWreq_e(exportName: string): Range | undefined { const wreq_e = this.findWreq_e(); if (!wreq_e) return undefined; const uses = this.vars.get(wreq_e); if (!uses) return undefined; const exportAssignment = uses.uses.find(({ location }) => { const [, moduleProp] = getLeadingIdentifier(location); return moduleProp?.text === "exports"; }); if (!exportAssignment) return undefined; const exportObject = findParent( exportAssignment.location, isBinaryExpression, )?.right; if (!exportObject || !isObjectLiteralExpression(exportObject)) return undefined; const exportItem = findObjectLiteralByKey(exportObject, exportName); if (!exportItem) return undefined; return this.makeRangeFromAstNode(exportItem.name ?? exportItem); } }