Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: analysis support for builtin modules #8

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Next Next commit
feat(WIP): resolve std libs about http
  • Loading branch information
werifu committed Mar 23, 2023
commit 3081bf72f183b8e01ae915792ad85811d6289704
81 changes: 35 additions & 46 deletions src/analysis/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import Solver from "./solver";
import {AnalysisState, globalLoc} from "./analysisstate";
import {DummyModuleInfo, FunctionInfo, ModuleInfo, normalizeModuleName, PackageInfo} from "./infos";
import logger from "../misc/logger";
import {builtinModules} from "../natives/nodejs";
import {requireResolve} from "../misc/files";
import {options} from "../options";
import {FilePath, getOrSet, isArrayIndex, sourceLocationToStringWithFile} from "../misc/util";
Expand Down Expand Up @@ -384,59 +383,49 @@ export class Operations {
requireModule(str: string, resultVar: ConstraintVar | undefined, path: NodePath): ModuleInfo | DummyModuleInfo | undefined { // see requireModule in modulefinder.ts
const reexport = isExportDeclaration(path.node);
let m: ModuleInfo | DummyModuleInfo | undefined;
if (builtinModules.has(str) || (str.startsWith("node:") && builtinModules.has(str.substring(5)))) {

if (!reexport) {
// standard library module: model with UnknownAccessPath
// constraint: @Unknown ∈ ⟦require(...)⟧
this.solver.addAccessPath(UnknownAccessPath.instance, resultVar);
// TODO: models for parts of the standard library
} else
this.a.warnUnsupported(path.node, `Ignoring re-export from built-in module '${str}'`); // TODO: re-exporting from built-in module
} else {
try {
try {

// try to locate the module
const filepath = requireResolve(str, this.file, path.node.loc, this.a);
if (filepath) {
// try to locate the module
const filepath = requireResolve(str, this.file, path.node.loc, this.a);
if (filepath) {

// register that the module is reached
m = this.a.reachedFile(filepath, path.getFunctionParent()?.node ?? this.file);
// register that the module is reached
m = this.a.reachedFile(filepath, path.getFunctionParent()?.node ?? this.file);

if (!reexport) {
// constraint: ⟦module_m.exports⟧ ⊆ ⟦require(...)⟧ where m denotes the module being loaded
this.solver.addSubsetConstraint(this.varProducer.objPropVar(this.a.canonicalizeToken(new NativeObjectToken("module", m)), "exports"), resultVar);
}
if (!reexport) {
// constraint: ⟦module_m.exports⟧ ⊆ ⟦require(...)⟧ where m denotes the module being loaded
this.solver.addSubsetConstraint(this.varProducer.objPropVar(this.a.canonicalizeToken(new NativeObjectToken("module", m)), "exports"), resultVar);
}
} catch {
if (options.ignoreUnresolved || options.ignoreDependencies) {
if (logger.isVerboseEnabled())
logger.verbose(`Ignoring unresolved module '${str}' at ${sourceLocationToStringWithFile(path.node.loc)}`);
} else // TODO: special warning if the require/import is placed in a try-block, an if statement, or a switch case?
this.a.warn(`Unable to resolve module '${str}' at ${sourceLocationToStringWithFile(path.node.loc)}`); // TODO: may report duplicate error messages

// couldn't find module file (probably hasn't been installed), use a DummyModuleInfo if absolute module name
if (!"./#".includes(str[0]))
m = getOrSet(this.a.dummyModuleInfos, str, () => new DummyModuleInfo(str));
}
} catch {
if (options.ignoreUnresolved || options.ignoreDependencies) {
if (logger.isVerboseEnabled())
logger.verbose(`Ignoring unresolved module '${str}' at ${sourceLocationToStringWithFile(path.node.loc)}`);
} else // TODO: special warning if the require/import is placed in a try-block, an if statement, or a switch case?
this.a.warn(`Unable to resolve module '${str}' at ${sourceLocationToStringWithFile(path.node.loc)}`); // TODO: may report duplicate error messages

// couldn't find module file (probably hasn't been installed), use a DummyModuleInfo if absolute module name
if (!"./#".includes(str[0]))
m = getOrSet(this.a.dummyModuleInfos, str, () => new DummyModuleInfo(str));
}

if (m) {

// add access path token
const analyzed = m instanceof ModuleInfo && (!options.ignoreDependencies || this.a.entryFiles.has(m.path));
if (!analyzed || options.vulnerabilities) {
const s = normalizeModuleName(str);
const tracked = options.trackedModules && options.trackedModules.find(e =>
micromatch.isMatch(m!.getOfficialName(), e) || micromatch.isMatch(s, e))
this.solver.addAccessPath(tracked ?
this.a.canonicalizeAccessPath(new ModuleAccessPath(m, s)) :
IgnoredAccessPath.instance,
resultVar);
}

this.a.registerRequireCall(path.node, this.a.getEnclosingFunctionOrModule(path, this.moduleInfo), m);
if (m) {

// add access path token
const analyzed = m instanceof ModuleInfo && (!options.ignoreDependencies || this.a.entryFiles.has(m.path));
if (!analyzed || options.vulnerabilities) {
const s = normalizeModuleName(str);
const tracked = options.trackedModules && options.trackedModules.find(e =>
micromatch.isMatch(m!.getOfficialName(), e) || micromatch.isMatch(s, e))
this.solver.addAccessPath(tracked ?
this.a.canonicalizeAccessPath(new ModuleAccessPath(m, s)) :
IgnoredAccessPath.instance,
resultVar);
}

this.a.registerRequireCall(path.node, this.a.getEnclosingFunctionOrModule(path, this.moduleInfo), m);
}

return m;
}

Expand Down
31 changes: 24 additions & 7 deletions src/misc/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {findPackageJson} from "./packagejson";
import {AnalysisState} from "../analysis/analysisstate";
import {tsResolveModuleName} from "../typescript/moduleresolver";
import stringify from "stringify2stream";
import {builtinModules} from "../natives/nodejs";

/**
* Expands the given list of file paths.
Expand Down Expand Up @@ -97,7 +98,16 @@ export function requireResolve(str: string, file: FilePath, loc: SourceLocation
}
let filepath;
try {
filepath = tsResolveModuleName(str, file);
if (builtinModules.has(str)) {
// mock the behavior of tsResolveModuleName for builtins like `require('http')`
filepath = resolveBuiltinModule(str);
} else if (str.startsWith("node:") && builtinModules.has(str.substring(5))) {
// mock the behavior of tsResolveModuleName for builtins like `require('node:http')`
filepath = resolveBuiltinModule(str.substring(5))
} else {
filepath = tsResolveModuleName(str, file);
}

// TypeScript prioritizes .ts over .js, overrule if coming from a .js file
if (file.endsWith(".js") && filepath.endsWith(".ts") && !str.endsWith(".ts")) {
const p = filepath.substring(0, filepath.length - 3) + ".js";
Expand All @@ -119,11 +129,7 @@ export function requireResolve(str: string, file: FilePath, loc: SourceLocation
if (!filepath)
throw e;
}
if (!filepath.startsWith(options.basedir)) {
const msg = `Found module at ${filepath}, but not in basedir`;
logger.debug(msg);
throw new Error(msg);
}

if (!filepath.endsWith(".js") && !filepath.endsWith(".jsx") && !filepath.endsWith(".es") && !filepath.endsWith(".mjs") &&
!filepath.endsWith(".cjs") && !filepath.endsWith(".ts") && !filepath.endsWith(".tsx")) {
a.warn(`Module '${filepath}' at ${sourceLocationToStringWithFile(loc)} has unrecognized extension, skipping it`);
Expand Down Expand Up @@ -217,4 +223,15 @@ export function writeStreamedStringify(value: any,
if (chunk)
writeSync(fd, chunk);
}, replacer, space);
}
}

/**
* Resolve the path of standard module like 'http', 'fs' etc to a local file.
*/
export function resolveBuiltinModule(moduleName: string): FilePath {
const filepath = resolve(__dirname, `../mockbuiltin/${moduleName}.js`);
if (!existsSync(filepath)) {
throw new Error;
}
return filepath;
}
17 changes: 13 additions & 4 deletions src/misc/packagejson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,19 @@ export function getPackageJsonInfo(tofile: FilePath): PackageJsonInfo {
}
}
} else {
name = "<main>";
packagekey = "<unknown>";
version = undefined;
dir = ".";
if (dirname(tofile) === resolve(__dirname, '../mockbuiltin/')) {
// handle builtin modules in the graph
name = "<builtin modules>";
packagekey = "<unknown>";
version = undefined;
dir = "./src/mockbuiltin";
} else {
logger.debug(`Unknown package of tofile ${tofile}`);
name = "<main>";
packagekey = "<unknown>";
version = undefined;
dir = ".";
}
}
return {packagekey, name, version, main, dir}
}
77 changes: 77 additions & 0 deletions src/mockbuiltin/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
class EventEmitter {
constructor(...args) {
for (const arg of args) arg();
}
setMaxListeners(...args) {
for (const arg of args) arg();
return this;
}
getMaxListeners(...args) {
for (const arg of args) arg();
}
emit(...args) {
for (const arg of args) arg();
}
addListener(...args) {
for (const arg of args) arg();
return this;
}
on(...args) {
for (const arg of args) arg();
return this;
}
prependListener(...args) {
for (const arg of args) arg();
return this;
}
once(...args) {
for (const arg of args) arg();
return this;
}
prependOnceListener(...args) {
for (const arg of args) arg();
return this;
}
removeListener(...args) {
for (const arg of args) arg();
return this;
}
off(...args) {
for (const arg of args) arg();
return this;
}
removeAllListeners(...args) {
for (const arg of args) arg();
}
listeners(...args) {
for (const arg of args) arg();
}
rawListeners(...args) {
for (const arg of args) arg();
}
listenerCount(...args) {
for (const arg of args) arg();
}
eventNames(...args) {
for (const arg of args) arg();
}
}

function once(...args) {
for (const arg of args) arg();
}

function on(...args) {
for (const arg of args) arg();
}

function getEventListeners(...args) {
for (const arg of args) arg();
}

module.exports = {
EventEmitter,
once,
on,
getEventListeners,
};
Loading