@syntest/analysis-javascript
Version:
SynTest CFG JavaScript is a library for generating control flow graphs for the JavaScript language
634 lines • 27.8 kB
JavaScript
"use strict";
/*
* Copyright 2020-2023 Delft University of Technology and SynTest contributors
*
* This file is part of SynTest Framework - SynTest Javascript.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TargetVisitor = void 0;
const analysis_1 = require("@syntest/analysis");
const ast_visitor_javascript_1 = require("@syntest/ast-visitor-javascript");
const logging_1 = require("@syntest/logging");
const diagnostics_1 = require("../utils/diagnostics");
const COMPUTED_FLAG = ":computed:";
class TargetVisitor extends ast_visitor_javascript_1.AbstractSyntaxTreeVisitor {
constructor(filePath, syntaxForgiving, exports) {
super(filePath, syntaxForgiving);
this.FunctionDeclaration = (path) => {
// e.g. function x() {}
const targetName = this._getTargetNameOfDeclaration(path);
const id = this._getNodeId(path);
const export_ = this._getExport(id);
this._extractFromFunction(path, id, id, targetName, export_, false, false);
path.skip();
};
this.ClassDeclaration = (path) => {
// e.g. class A {}
const targetName = this._getTargetNameOfDeclaration(path);
const id = this._getNodeId(path);
const export_ = this._getExport(id);
this._extractFromClass(path, id, id, targetName, export_);
path.skip();
};
this.FunctionExpression = (path) => {
// only thing left where these can be found is:
// call(function () {})
const targetName = this._getTargetNameOfExpression(path);
const id = this._getNodeId(path);
const export_ = this._getExport(id);
this._extractFromFunction(path, id, id, targetName, export_, false, false);
path.skip();
};
this.ClassExpression = (path) => {
// only thing left where these can be found is:
// call(class {})
const targetName = this._getTargetNameOfExpression(path);
const id = this._getNodeId(path);
const export_ = this._getExport(id);
this._extractFromClass(path, id, id, targetName, export_);
path.skip();
};
this.ArrowFunctionExpression = (path) => {
// only thing left where these can be found is:
// call(() => {})
const targetName = this._getTargetNameOfExpression(path);
// TODO is there a difference if the parent is a variable declarator?
const id = this._getNodeId(path);
const export_ = this._getExport(id);
this._extractFromFunction(path, id, id, targetName, export_, false, false);
path.skip();
};
this.VariableDeclarator = (path) => {
if (!path.has("init")) {
path.skip();
return;
}
const idPath = path.get("id");
const init = path.get("init");
const targetName = idPath.node.name;
const id = this._getNodeId(path);
const typeId = this._getNodeId(init);
const export_ = this._getExport(id);
if (init.isFunction()) {
this._extractFromFunction(init, id, typeId, targetName, export_, false, false);
}
else if (init.isClass()) {
this._extractFromClass(init, id, typeId, targetName, export_);
}
else if (init.isObjectExpression()) {
this._extractFromObjectExpression(init, id, typeId, targetName, export_);
}
else {
// TODO
}
path.skip();
};
this.AssignmentExpression = (path) => {
const left = path.get("left");
const right = path.get("right");
if (!right.isFunction() &&
!right.isClass() &&
!right.isObjectExpression()) {
return;
}
const targetName = this._getTargetNameOfExpression(right);
let isObject = false;
let isMethod = false;
let objectId;
let id = this._getBindingId(left);
if (left.isMemberExpression()) {
const object = left.get("object");
const property = left.get("property");
if (left.get("property").isIdentifier() && left.node.computed) {
TargetVisitor.LOGGER.warn("We do not support dynamic computed properties: x[a] = ?");
path.skip();
return;
}
else if (!left.get("property").isIdentifier() && !left.node.computed) {
// we also dont support a.f() = ?
// or equivalent
path.skip();
return;
}
if (object.isIdentifier()) {
// x.? = ?
// x['?'] = ?
if (object.node.name === "exports" ||
(object.node.name === "module" &&
property.isIdentifier() &&
property.node.name === "exports")) {
// exports.? = ?
// module.exports = ?
isObject = false;
id = this._getBindingId(right);
}
else {
isObject = true;
objectId = this._getBindingId(object);
// find object
const objectTarget = this._subTargets.find((value) => value.id === objectId && value.type === analysis_1.TargetType.OBJECT);
if (!objectTarget) {
const export_ = this._getExport(objectId);
// create one if it does not exist
const objectTarget = {
id: objectId,
typeId: objectId,
name: object.node.name,
type: analysis_1.TargetType.OBJECT,
exported: !!export_,
default: export_ ? export_.default : false,
module: export_ ? export_.module : false,
};
this._subTargets.push(objectTarget);
}
}
}
else if (object.isMemberExpression()) {
// ?.?.? = ?
const subObject = object.get("object");
const subProperty = object.get("property");
// what about module.exports.x
if (subObject.isIdentifier() &&
subProperty.isIdentifier() &&
subProperty.node.name === "prototype") {
// x.prototype.? = ?
objectId = this._getBindingId(subObject);
const objectTarget = (this._subTargets.find((value) => value.id === objectId));
const newTargetClass = {
id: objectTarget.id,
type: analysis_1.TargetType.CLASS,
name: objectTarget.name,
typeId: objectTarget.id,
exported: objectTarget.exported,
renamedTo: objectTarget.renamedTo,
module: objectTarget.module,
default: objectTarget.default,
};
// replace original target by prototype class
this._subTargets[this._subTargets.indexOf(objectTarget)] =
newTargetClass;
const constructorTarget = {
id: objectTarget.id,
type: analysis_1.TargetType.METHOD,
name: objectTarget.name,
typeId: objectTarget.id,
methodType: "constructor",
classId: objectTarget.id,
visibility: "public",
isStatic: false,
isAsync: "isAsync" in objectTarget
? objectTarget.isAsync
: false,
};
this._subTargets.push(constructorTarget);
isMethod = true;
}
}
else {
path.skip();
return;
}
}
const typeId = this._getNodeId(right);
const export_ = this._getExport(isObject ? objectId : id);
if (right.isFunction()) {
this._extractFromFunction(right, id, typeId, targetName, export_, isObject, isMethod, objectId);
}
else if (right.isClass()) {
this._extractFromClass(right, id, typeId, targetName, export_);
}
else if (right.isObjectExpression()) {
this._extractFromObjectExpression(right, id, typeId, targetName, export_);
}
else {
// TODO
}
path.skip();
};
TargetVisitor.LOGGER = (0, logging_1.getLogger)("TargetVisitor");
this._exports = exports;
this._subTargets = [];
}
_getExport(id) {
return this._exports.find((x) => {
return x.id === id;
});
}
_getTargetNameOfDeclaration(path) {
if (path.node.id === null) {
if (path.parentPath.node.type === "ExportDefaultDeclaration") {
// e.g. export default class {}
// e.g. export default function () {}
return "default";
}
else {
// e.g. class {}
// e.g. function () {}
// Should not be possible
throw new Error("unknown class declaration");
}
}
else {
// e.g. class x {}
// e.g. function x() {}
return path.node.id.name;
}
}
/**
* Get the target name of an expression
* The variable the expression is assigned to is used as the target name
* @param path
* @returns
*/
_getTargetNameOfExpression(path) {
// e.g. const x = class A {}
// e.g. const x = function A {}
// e.g. const x = () => {}
// we always use x as the target name instead of A
const parentNode = path.parentPath.node;
switch (parentNode.type) {
case "VariableDeclarator": {
// e.g. const ?? = class {}
// e.g. const ?? = function {}
// e.g. const ?? = () => {}
if (parentNode.id.type === "Identifier") {
// e.g. const x = class {}
// e.g. const x = function {}
// e.g. const x = () => {}
return parentNode.id.name;
}
else {
// e.g. const {x} = class {}
// e.g. const {x} = function {}
// e.g. const {x} = () => {}
// Should not be possible
throw new Error((0, diagnostics_1.unsupportedSyntax)(path.node.type, this._getNodeId(path)));
}
}
case "AssignmentExpression": {
// e.g. ?? = class {}
// e.g. ?? = function {}
// e.g. ?? = () => {}
const assigned = parentNode.left;
if (assigned.type === "Identifier") {
// could also be memberexpression
// e.g. x = class {}
// e.g. x = function {}
// e.g. x = () => {}
return assigned.name;
}
else if (assigned.type === "MemberExpression") {
// e.g. x.? = class {}
// e.g. x.? = function {}
// e.g. x.? = () => {}
if (assigned.computed === true) {
if (assigned.property.type.includes("Literal")) {
// e.g. x["y"] = class {}
// e.g. x["y"] = function {}
// e.g. x["y"] = () => {}
return "value" in assigned.property
? assigned.property.value.toString()
: "null";
}
else {
// e.g. x[y] = class {}
// e.g. x[y] = function {}
// e.g. x[y] = () => {}
// TODO unsupported cannot get the name unless executing
TargetVisitor.LOGGER.warn(`This tool does not support computed property assignments. Found one at ${this._getNodeId(path)}`);
return COMPUTED_FLAG;
}
}
else if (assigned.property.type === "Identifier") {
// e.g. x.y = class {}
// e.g. x.y = function {}
// e.g. x.y = () => {}
if (assigned.property.name === "exports" &&
assigned.object.type === "Identifier" &&
assigned.object.name === "module") {
// e.g. module.exports = class {}
// e.g. module.exports = function {}
// e.g. module.exports = () => {}
return "id" in parentNode.right
? parentNode.right.id.name
: "anonymousFunction";
}
return assigned.property.name;
}
else {
// e.g. x.? = class {}
// e.g. x.? = function {}
// e.g. x.? = () => {}
// Should not be possible
throw new Error((0, diagnostics_1.unsupportedSyntax)(path.node.type, this._getNodeId(path)));
}
}
else {
// e.g. {x} = class {}
// e.g. {x} = function {}
// e.g. {x} = () => {}
// Should not be possible
throw new Error((0, diagnostics_1.unsupportedSyntax)(path.node.type, this._getNodeId(path)));
}
}
case "ClassProperty":
// e.g. class A { ? = class {} }
// e.g. class A { ? = function () {} }
// e.g. class A { ? = () => {} }
case "ObjectProperty": {
// e.g. {?: class {}}
// e.g. {?: function {}}
// e.g. {?: () => {}}
if (parentNode.key.type === "Identifier") {
// e.g. class A { x = class {} }
// e.g. class A { x = function () {} }
// e.g. class A { x = () => {} }
// e.g. {y: class {}}
// e.g. {y: function {}}
// e.g. {y: () => {}}
return parentNode.key.name;
}
else if (parentNode.key.type.includes("Literal")) {
// e.g. class A { "x" = class {} }
// e.g. class A { "x" = function () {} }
// e.g. class A { "x" = () => {} }
// e.g. {1: class {}}
// e.g. {1: function {}}
// e.g. {1: () => {}}
return "value" in parentNode.key
? parentNode.key.value.toString()
: "null";
}
else {
// e.g. const {x} = class {}
// e.g. const {x} = function {}
// e.g. const {x} = () => {}
// e.g. {?: class {}}
// e.g. {?: function {}}
// e.g. {?: () => {}}
// Should not be possible
throw new Error((0, diagnostics_1.unsupportedSyntax)(path.node.type, this._getNodeId(path)));
}
}
case "ReturnStatement":
// e.g. return class {}
// e.g. return function () {}
// e.g. return () => {}
case "ArrowFunctionExpression":
// e.g. () => class {}
// e.g. () => function () {}
// e.g. () => () => {}
case "NewExpression":
// e.g. new Class(class {}) // dont think this one is possible but unsure
// e.g. new Class(function () {})
// e.g. new Class(() => {})
case "CallExpression": {
// e.g. function(class {}) // dont think this one is possible but unsure
// e.g. function(function () {})
// e.g. function(() => {})
return "id" in path.node && path.node.id && "name" in path.node.id
? path.node.id.name
: "anonymous";
}
case "ConditionalExpression": {
// e.g. c ? class {} : b
// e.g. c ? function () {} : b
// e.g. c ? () => {} : b
return this._getTargetNameOfExpression(path.parentPath);
}
case "LogicalExpression": {
// e.g. c || class {}
// e.g. c || function () {}
// e.g. c || () => {}
return this._getTargetNameOfExpression(path.parentPath);
}
default: {
// e.g. class {}
// e.g. function () {}
// e.g. () => {}
// Should not be possible
throw new Error(`Unknown parent expression ${parentNode.type} for ${path.node.type} in ${this._getNodeId(path)}`);
}
}
}
_extractFromFunction(path, functionId, typeId, functionName, export_, isObjectFunction, isMethod, superId) {
let target;
if (isObjectFunction && isMethod) {
throw new Error("Cannot be method and object function");
}
if (isObjectFunction) {
if (!superId) {
throw new Error("if it is an object function the object id should be given");
}
target = {
id: functionId,
typeId: typeId,
objectId: superId,
name: functionName,
type: analysis_1.TargetType.OBJECT_FUNCTION,
isAsync: path.node.async,
};
}
else if (isMethod) {
if (!superId) {
throw new Error("if it is an object function the object id should be given");
}
target = {
id: functionId,
typeId: typeId,
classId: superId,
name: functionName,
type: analysis_1.TargetType.METHOD,
isAsync: path.node.async,
methodType: path.isClassMethod() ? path.node.kind : "method",
visibility: path.isClassMethod() && path.node.access
? path.node.access
: "public",
isStatic: path.isClassMethod() || path.isClassProperty()
? path.node.static
: false,
};
}
else {
target = {
id: functionId,
typeId: typeId,
name: functionName,
type: analysis_1.TargetType.FUNCTION,
exported: !!export_,
default: export_ ? export_.default : false,
module: export_ ? export_.module : false,
isAsync: path.node.async,
};
}
this._subTargets.push(target);
const body = path.get("body");
if (Array.isArray(body)) {
throw new TypeError("weird function body");
}
else {
body.visit();
}
}
_extractFromObjectExpression(path, objectId, typeId, objectName, export_) {
const target = {
id: objectId,
typeId: typeId,
name: objectName,
type: analysis_1.TargetType.OBJECT,
exported: !!export_,
default: export_ ? export_.default : false,
module: export_ ? export_.module : false,
};
this._subTargets.push(target);
// loop over object properties
for (const property of path.get("properties")) {
if (property.isObjectMethod()) {
if (property.node.key.type !== "Identifier") {
// e.g. class A { ?() {} }
// unsupported
// not possible i think
throw new Error("unknown class method key");
}
const targetName = property.node.key.name;
const id = this._getNodeId(property);
this._extractFromFunction(property, id, id, targetName, undefined, true, false, objectId);
}
else if (property.isObjectProperty()) {
const key = property.get("key");
const value = property.get("value");
if (value) {
const id = this._getNodeId(property);
let targetName;
if (key.isIdentifier()) {
targetName = key.node.name;
}
else if (key.isStringLiteral() ||
key.isBooleanLiteral() ||
key.isNumericLiteral() ||
key.isBigIntLiteral()) {
targetName = String(key.node.value);
}
if (value.isFunction()) {
this._extractFromFunction(value, id, id, targetName, undefined, true, false, objectId);
}
else if (value.isClass()) {
this._extractFromClass(value, id, id, targetName);
}
else if (value.isObjectExpression()) {
this._extractFromObjectExpression(value, id, id, targetName);
}
else {
// TODO
}
}
}
else if (property.isSpreadElement()) {
// TODO
// extract the spread element
}
}
}
_extractFromClass(path, classId, typeId, className, export_) {
const target = {
id: classId,
typeId: typeId,
name: className,
type: analysis_1.TargetType.CLASS,
exported: !!export_,
default: export_ ? export_.default : false,
module: export_ ? export_.module : false,
};
this._subTargets.push(target);
const body = path.get("body");
for (const classBodyAttribute of body.get("body")) {
if (classBodyAttribute.isClassMethod()) {
if (classBodyAttribute.node.key.type !== "Identifier") {
// e.g. class A { ?() {} }
// unsupported
// not possible i think
throw new Error("unknown class method key");
}
const targetName = classBodyAttribute.node.key.name;
const id = this._getNodeId(classBodyAttribute);
this._extractFromFunction(classBodyAttribute, id, id, targetName, undefined, false, true, classId);
}
else if (classBodyAttribute.isClassProperty()) {
const key = classBodyAttribute.get("key");
const value = classBodyAttribute.get("value");
if (value) {
const id = this._getNodeId(classBodyAttribute);
let targetName;
if (key.isIdentifier()) {
targetName = key.node.name;
}
else if (key.isStringLiteral() ||
key.isBooleanLiteral() ||
key.isNumericLiteral() ||
key.isBigIntLiteral()) {
targetName = String(key.node.value);
}
if (value.isFunction()) {
this._extractFromFunction(value, id, id, targetName, undefined, false, true, classId);
}
else if (value.isClass()) {
this._extractFromClass(value, id, id, targetName);
}
else if (value.isObjectExpression()) {
this._extractFromObjectExpression(value, id, id, targetName);
}
else {
// TODO
}
}
}
else {
TargetVisitor.LOGGER.warn(`Unsupported class body attribute: ${classBodyAttribute.node.type}`);
}
}
}
get subTargets() {
return this._subTargets
.reverse()
.filter((subTarget, index, self) => {
if (!("name" in subTarget)) {
// paths/branches/lines are always unique
return true;
}
// filter duplicates because of redefinitions
// e.g. let a = 1; a = 2;
// this would result in two subtargets with the same name "a"
// but we only want the last one
return (index ===
self.findIndex((t) => {
return ("name" in t &&
t.id === subTarget.id &&
t.type === subTarget.type &&
t.name === subTarget.name &&
(t.type === analysis_1.TargetType.METHOD
? t.methodType ===
subTarget.methodType &&
t.isStatic ===
subTarget.isStatic &&
t.classId ===
subTarget.classId
: true));
}));
})
.reverse();
}
}
exports.TargetVisitor = TargetVisitor;
//# sourceMappingURL=TargetVisitor.js.map