@syntest/analysis-javascript
Version:
SynTest CFG JavaScript is a library for generating control flow graphs for the JavaScript language
436 lines • 18.9 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.TypeModel = void 0;
const prng_1 = require("@syntest/prng");
const Type_1 = require("./Type");
const TypeEnum_1 = require("./TypeEnum");
class TypeModel {
constructor() {
this._elements = new Set();
this._relationScoreMap = new Map();
this._elementTypeScoreMap = new Map();
this._typeExecutionScoreMap = new Map();
this._elementTypeProbabilityMap = new Map();
this._scoreHasChangedMap = new Map();
this._objectTypeDescription = new Map();
this.addId("anon"); // should be removed at some point
}
get relationScoreMap() {
return this._relationScoreMap;
}
get elementTypeScoreMap() {
return this._elementTypeScoreMap;
}
get typeExecutionScoreMap() {
return this._typeExecutionScoreMap;
}
getObjectDescription(element) {
if (!this._objectTypeDescription.has(element)) {
throw new Error(`Element ${element} does not have an object description`);
}
return this._objectTypeDescription.get(element);
}
addId(id) {
if (this._elements.has(id)) {
return;
}
this._elements.add(id);
this._relationScoreMap.set(id, new Map());
this._elementTypeScoreMap.set(id, new Map());
this._elementTypeProbabilityMap.set(id, new Map());
this._typeExecutionScoreMap.set(id, new Map());
this._scoreHasChangedMap.set(id, true);
this._objectTypeDescription.set(id, {
properties: new Map(),
elements: new Set(),
parameters: new Map(),
parameterNames: new Map(),
return: new Set(),
});
}
setEqual(id1, id2) {
//TODO maybe merge
for (const [key, value] of this._relationScoreMap.get(id2).entries())
this._relationScoreMap.get(id1).has(key)
? this._relationScoreMap
.get(id1)
.set(key, this._relationScoreMap.get(id1).get(key) + value)
: this._relationScoreMap.get(id1).set(key, value);
for (const [key, value] of this._elementTypeScoreMap.get(id2).entries())
this._elementTypeScoreMap.get(id1).has(key)
? this._elementTypeScoreMap
.get(id1)
.set(key, this._elementTypeScoreMap.get(id1).get(key) + value)
: this._elementTypeScoreMap.get(id1).set(key, value);
for (const [key, value] of this._elementTypeProbabilityMap
.get(id2)
.entries())
this._elementTypeProbabilityMap.get(id1).has(key)
? this._elementTypeProbabilityMap
.get(id1)
.set(key, this._elementTypeProbabilityMap.get(id1).get(key) + value)
: this._elementTypeProbabilityMap.get(id1).set(key, value);
for (const [key, value] of this._typeExecutionScoreMap.get(id2).entries())
this._typeExecutionScoreMap.get(id1).has(key)
? this._typeExecutionScoreMap
.get(id1)
.set(key, this._typeExecutionScoreMap.get(id1).get(key) + value)
: this._typeExecutionScoreMap.get(id1).set(key, value);
this._relationScoreMap.set(id2, this._relationScoreMap.get(id1));
this._elementTypeScoreMap.set(id2, this._elementTypeScoreMap.get(id1));
this._elementTypeProbabilityMap.set(id2, this._elementTypeProbabilityMap.get(id1));
this._typeExecutionScoreMap.set(id2, this._typeExecutionScoreMap.get(id1));
this._scoreHasChangedMap.set(id2, this._scoreHasChangedMap.get(id1));
// TODO maybe this should be merged too?
// or should we keep them separate?
this._objectTypeDescription.set(id2, this._objectTypeDescription.get(id1));
}
_addRelationScore(id1, id2, score) {
if (!this._relationScoreMap.has(id1)) {
throw new Error(`Element ${id1} does not exist`);
}
if (!this._relationScoreMap.get(id1).has(id2)) {
this._relationScoreMap.get(id1).set(id2, 0);
}
const currentScore1 = this._relationScoreMap.get(id1).get(id2);
this._relationScoreMap.get(id1).set(id2, currentScore1 + score);
this._scoreHasChangedMap.set(id1, true);
}
addWeakRelation(id1, id2) {
this.addRelationScore(id1, id2, 1);
}
addStrongRelation(id1, id2) {
this.addRelationScore(id1, id2, 3);
}
addRelationScore(id1, id2, score) {
if (id1 === id2) {
// no self loops
return;
// throw new Error(`ids should not be equal to add a relation id: ${id1}`);
}
this._addRelationScore(id1, id2, score);
this._addRelationScore(id2, id1, score);
}
addStrongTypeScore(id, type) {
this.addTypeScore(id, type, 5);
}
addTypeScore(id, type, score = 1) {
if (!this._elementTypeScoreMap.has(id)) {
throw new Error(`Element ${id} does not exist`);
}
if (!this._elementTypeScoreMap.get(id).has(type)) {
this._elementTypeScoreMap.get(id).set(type, 0);
}
if (!this._typeExecutionScoreMap.get(id).has(type)) {
this._typeExecutionScoreMap.get(id).set(type, 0);
}
const currentScore = this._elementTypeScoreMap.get(id).get(type);
this._elementTypeScoreMap.get(id).set(type, currentScore + score);
this._scoreHasChangedMap.set(id, true);
if (type === TypeEnum_1.TypeEnum.NUMERIC) {
this.addTypeScore(id, TypeEnum_1.TypeEnum.INTEGER, score);
}
}
addPropertyType(element, property, id) {
// check if the property is from a string/array/function
if (Type_1.functionProperties.has(property)) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.FUNCTION);
}
if (Type_1.arrayProperties.has(property)) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.ARRAY);
}
if (Type_1.stringProperties.has(property)) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.STRING);
}
this.addTypeScore(element, TypeEnum_1.TypeEnum.OBJECT);
this.getObjectDescription(element).properties.set(property, id);
}
addParameterType(element, index, id, name) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.FUNCTION);
this.getObjectDescription(element).parameters.set(index, id);
this.getObjectDescription(element).parameterNames.set(index, name);
}
addReturnType(element, returnId) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.FUNCTION);
this.getObjectDescription(element).return.add(returnId);
}
addElementType(element, id) {
this.addTypeScore(element, TypeEnum_1.TypeEnum.ARRAY);
this.getObjectDescription(element).elements.add(id);
}
// TODO should also add scores to the relations when relevant
addExecutionScore(id, typeId, typeEnum, score = -1) {
if (!this._typeExecutionScoreMap.has(id)) {
throw new Error(`Element ${id} does not exist`);
}
let type = typeEnum;
if (id !== typeId) {
type = `${typeId}<>${typeEnum}`;
}
if (!this._typeExecutionScoreMap.get(id).has(type)) {
this._typeExecutionScoreMap.get(id).set(type, 0);
}
if (!this._elementTypeScoreMap.get(id).has(type)) {
this._elementTypeScoreMap.get(id).set(type, 0);
}
const currentScore = this._typeExecutionScoreMap.get(id).get(type);
this._typeExecutionScoreMap.get(id).set(type, currentScore + score);
this._scoreHasChangedMap.set(id, true);
}
_sum(iterable) {
return [...iterable].reduce((total, currentValue) => total + currentValue, 0);
}
/**
*
* @param incorporateExecutionScore wether the execution score should be weighted in
* @param id the id we want to get a random type for
* @param matchType (optional) the type enum you want to get (there can be multiple object/function/array types)
* @returns a string describing the type
*/
getRandomType(incorporateExecutionScore, randomTypeProbability, id) {
const probabilities = this.calculateProbabilitiesForElement(incorporateExecutionScore, id);
// const x = new Map();
// for (const [type, probability] of probabilities.entries()) {
// const typeEnum = type.includes("<>") ? type.split("<>")[1] : type;
// if (!x.has(typeEnum)) {
// x.set(typeEnum, 0);
// }
// x.set(typeEnum, x.get(typeEnum) + probability);
// }
// console.log(id);
// console.log(x);
const genericTypes = [
TypeEnum_1.TypeEnum.ARRAY,
TypeEnum_1.TypeEnum.BOOLEAN,
TypeEnum_1.TypeEnum.FUNCTION,
TypeEnum_1.TypeEnum.NULL,
TypeEnum_1.TypeEnum.NUMERIC,
TypeEnum_1.TypeEnum.INTEGER,
TypeEnum_1.TypeEnum.OBJECT,
TypeEnum_1.TypeEnum.REGEX,
TypeEnum_1.TypeEnum.STRING,
TypeEnum_1.TypeEnum.UNDEFINED,
];
if (probabilities.size === 0) {
return prng_1.prng.pickOne(genericTypes);
}
if (this._sum(probabilities.values()) === 0 ||
prng_1.prng.nextBoolean(randomTypeProbability)) {
return prng_1.prng.pickOne([
...new Set([...probabilities.keys(), ...genericTypes]),
]);
}
const matchingTypes = [...probabilities.entries()];
const totalProbability = 1;
const choice = prng_1.prng.nextDouble(0, totalProbability);
let index = 0;
let chosenType;
let probability;
for ([chosenType, probability] of matchingTypes) {
if (choice <= index + probability) {
return chosenType;
}
index += probability;
}
return chosenType;
}
getHighestProbabilityType(incorporateExecutionScore, randomTypeProbability, id) {
const probabilities = this.calculateProbabilitiesForElement(incorporateExecutionScore, id);
const genericTypes = [
TypeEnum_1.TypeEnum.ARRAY,
TypeEnum_1.TypeEnum.BOOLEAN,
TypeEnum_1.TypeEnum.FUNCTION,
TypeEnum_1.TypeEnum.NULL,
TypeEnum_1.TypeEnum.NUMERIC,
TypeEnum_1.TypeEnum.INTEGER,
TypeEnum_1.TypeEnum.OBJECT,
TypeEnum_1.TypeEnum.REGEX,
TypeEnum_1.TypeEnum.STRING,
TypeEnum_1.TypeEnum.UNDEFINED,
];
if (probabilities.size === 0) {
return prng_1.prng.pickOne(genericTypes);
}
if (prng_1.prng.nextBoolean(randomTypeProbability)) {
return prng_1.prng.pickOne([
...new Set([...probabilities.keys(), ...genericTypes]),
]);
}
const matchingTypes = probabilities;
let best = [...matchingTypes.keys()][0];
for (const [type, probability] of matchingTypes.entries()) {
if (probability > matchingTypes.get(best)) {
best = type;
}
}
return best;
}
calculateProbabilitiesForFile(incorporateExecutionScore, filepath) {
const map = new Map();
for (const id of this._elements) {
if (!id.startsWith(filepath)) {
continue;
}
map.set(id, this.calculateProbabilitiesForElement(incorporateExecutionScore, id));
}
return map;
}
calculateProbabilitiesForElement(incorporateExecutionScore, id, relationPairsVisited) {
// if (!this._scoreHasChangedMap.has(element)) {
// throw new Error(`Element ${element} does not exist`);
// }
// if (this._scoreHasChangedMap.get(element) === false) {
// // prevent recalculation of probabilities without score changes
// return this._elementTypeProbabilityMap.get(element);
// }
// this._scoreHasChangedMap.set(element, false);
let probabilityMap = new Map();
if (id === "anon") {
return probabilityMap;
}
const typeScoreMap = this._elementTypeScoreMap.get(id);
const relationMap = this._relationScoreMap.get(id);
if (typeScoreMap === undefined) {
throw new Error(`Cannot get typescoreMap of ${id}`);
}
if (!relationPairsVisited) {
relationPairsVisited = new Map();
// this._scoreHasChangedMap.set(element, false);
// this._elementTypeProbabilityMap.set(element, probabilityMap);
}
let totalScore = this._sum(typeScoreMap.values());
const usableRelations = new Set();
for (const [relation, score] of relationMap.entries()) {
if (relation === id) {
// ignore self references
continue;
}
if ((relationPairsVisited.has(id) &&
relationPairsVisited.get(id).has(relation)) ||
(relationPairsVisited.has(relation) &&
relationPairsVisited.get(relation).has(id))) {
// we have already visited this relation pair
// this means that we have a cycle in the graph
// we can safely ignore this relation
continue;
}
usableRelations.add(relation);
totalScore += score;
}
if (totalScore === 0) {
totalScore = 1;
}
for (const [type, score] of typeScoreMap.entries()) {
probabilityMap.set(type, score / totalScore);
}
for (const relation of usableRelations) {
probabilityMap = this.incorporateRelation(id, probabilityMap, relation, relationMap, relationPairsVisited, totalScore, incorporateExecutionScore);
}
// incorporate execution scores
probabilityMap = this.incorporateExecutionScores(id, probabilityMap, incorporateExecutionScore);
return this.normalizeProbabilities(probabilityMap);
}
incorporateRelation(id, probabilityMap, relation, relationMap, relationPairsVisited, totalScore, incorporateExecutionScore) {
const score = relationMap.get(relation);
if (!relationPairsVisited.has(id)) {
relationPairsVisited.set(id, new Set());
}
if (!relationPairsVisited.has(relation)) {
relationPairsVisited.set(relation, new Set());
}
relationPairsVisited.get(id).add(relation);
relationPairsVisited.get(relation).add(id);
const probabilityOfRelation = score / totalScore;
const probabilityMapOfRelation = this.calculateProbabilitiesForElement(incorporateExecutionScore, relation, relationPairsVisited);
for (const [type, probability] of probabilityMapOfRelation.entries()) {
let finalType = type;
if (!type.includes("<>")) {
// maybe should check for includes (or the inverse by checking for primitive types)
// this will only add only the final relation id
// the other method will add all relation id from the element to the final relation
finalType = `${relation}<>${type}`;
}
if (finalType.includes("<>") && finalType.split("<>")[0] === id) {
// skip this is a self loop
continue;
}
if (!probabilityMap.has(finalType)) {
probabilityMap.set(finalType, 0);
}
probabilityMap.set(finalType, probabilityMap.get(finalType) + probability * probabilityOfRelation);
}
return probabilityMap;
}
incorporateExecutionScores(id, probabilityMap, incorporateExecutionScore) {
const executionScoreMap = this._typeExecutionScoreMap.get(id);
if (!incorporateExecutionScore || executionScoreMap.size <= 1) {
return probabilityMap;
}
const combinedProbabilityMap = new Map();
let minValue = 0;
for (const score of executionScoreMap.values()) {
minValue = Math.min(minValue, score);
}
let totalScore = 0;
for (const type of probabilityMap.keys()) {
let score = executionScoreMap.get(type) ?? 0;
score -= minValue;
score += 1;
totalScore += score;
}
if (totalScore < 0) {
throw new Error("Total score should be positive but is negative");
}
if (totalScore === 0) {
throw new Error("Total score should be positive but is zero");
}
if (Number.isNaN(totalScore)) {
throw new TypeError("Total score should be positive but is NaN");
}
// incorporate execution score
for (const type of probabilityMap.keys()) {
let score = executionScoreMap.has(type) ? executionScoreMap.get(type) : 0;
score -= minValue;
score += 1;
const executionScoreDiscount = score / totalScore;
const probability = probabilityMap.get(type);
const newProbability = executionScoreDiscount * probability;
combinedProbabilityMap.set(type, newProbability);
}
return combinedProbabilityMap;
}
normalizeProbabilities(probabilityMap) {
// normalize to 1
let totalProbability = 0;
for (const probability of probabilityMap.values()) {
totalProbability += probability;
}
if (totalProbability === 0 || totalProbability === 1) {
return probabilityMap;
}
const normalizedProbabilityMap = new Map();
for (const [type, probability] of probabilityMap.entries()) {
normalizedProbabilityMap.set(type, probability / totalProbability);
}
return normalizedProbabilityMap;
}
}
exports.TypeModel = TypeModel;
//# sourceMappingURL=TypeModel.js.map