Analyzer.java
/*
* Copyright 2019-2022 Foreseeti AB <https://foreseeti.com>
*
* 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
*
* https://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.
*/
package org.mal_lang.lib;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class Analyzer {
private MalLogger LOGGER;
private Map<String, AST.Asset> assets = new LinkedHashMap<>();
private Map<String, Scope<AST.Variable>> assetVariables = new LinkedHashMap<>();
private Map<String, Scope<AST.Association>> fields = new LinkedHashMap<>();
private Map<String, Scope<AST.AttackStep>> steps = new LinkedHashMap<>();
private Set<AST.Variable> currentVariables = new LinkedHashSet<>();
private Map<AST.Variable, Integer> variableReferenceCount = new HashMap<>();
private Map<AST.Association, Map<String, Integer>> fieldReferenceCount = new HashMap<>();
private AST ast;
private boolean failed;
private Analyzer(AST ast, boolean verbose, boolean debug) {
Locale.setDefault(Locale.ROOT);
LOGGER = new MalLogger("ANALYZER", verbose, debug);
this.ast = ast;
}
public static void analyze(AST ast) throws CompilerException {
analyze(ast, false, false);
}
public static void analyze(AST ast, boolean verbose, boolean debug) throws CompilerException {
new Analyzer(ast, verbose, debug).analyzeLog();
}
private void analyzeLog() throws CompilerException {
try {
_analyze();
LOGGER.print();
} catch (CompilerException e) {
LOGGER.print();
throw e;
}
}
private void _analyze() throws CompilerException {
collectAssociations();
checkDefines();
checkCategories();
checkAssets();
checkMetas();
checkExtends(); // might throw
checkAbstract();
checkParents(); // might throw
checkSteps();
checkCIA();
checkTTC();
checkFields();
checkVariables();
checkReaches(); // might throw
checkAssociations(); // might throw
checkUnused();
if (failed) {
throw exception();
}
}
private void collectAssociations() {
for (AST.Association assoc : ast.getAssociations()) {
setupFieldReferenceCounts(assoc);
}
}
private void addVariableReference(AST.Variable variable) {
int oldval = variableReferenceCount.get(variable);
variableReferenceCount.put(variable, oldval + 1);
}
private void setupFieldReferenceCounts(AST.Association assoc) {
Map<String, Integer> fieldCounts = new HashMap<>();
fieldCounts.put(assoc.leftField.id, 0);
fieldCounts.put(assoc.rightField.id, 0);
fieldReferenceCount.put(assoc, fieldCounts);
}
private void addFieldReference(AST.Association assoc, AST.ID field) {
var fieldCounts = fieldReferenceCount.get(assoc);
int oldcount = fieldCounts.get(field.id);
fieldCounts.put(field.id, oldcount + 1);
}
private void checkAssociations() throws CompilerException {
boolean err = false;
for (AST.Association assoc : ast.getAssociations()) {
if (!assets.containsKey(assoc.leftAsset.id)) {
error(assoc.leftAsset, String.format("Left asset '%s' is not defined", assoc.leftAsset.id));
err = true;
}
if (!assets.containsKey(assoc.rightAsset.id)) {
error(
assoc.rightAsset,
String.format("Right asset '%s' is not defined", assoc.rightAsset.id));
err = true;
}
}
if (err) {
throw exception();
}
}
private void checkUnused() {
// variables
for (AST.Variable variable : variableReferenceCount.keySet()) {
int val = variableReferenceCount.get(variable);
if (val == 0) {
LOGGER.warning(
variable.name, String.format("Variable '%s' is never used", variable.name.id));
}
}
// fields
for (var assoc : fieldReferenceCount.keySet()) {
var fieldCounts = fieldReferenceCount.get(assoc);
boolean onlyZeroRefs = true;
for (var field : fieldCounts.keySet()) {
int val = fieldCounts.get(field);
if (val > 0) {
onlyZeroRefs = false;
break;
}
}
if (onlyZeroRefs) {
LOGGER.warning(
assoc, String.format("Association '%s' is never used", assoc.toShortString()));
}
}
}
private void checkDefines() {
Map<String, AST.Define> defines = new HashMap<>();
for (AST.Define define : ast.getDefines()) {
AST.Define prevDef = defines.put(define.key.id, define);
if (prevDef != null) {
error(
define,
String.format(
"Define '%s' previously defined at %s", define.key.id, prevDef.posString()));
}
}
AST.Define id = defines.get("id");
if (id != null) {
if (id.value.isBlank()) {
error(id, "Define 'id' cannot be empty");
}
} else {
error("Missing required define '#id: \"\"'");
}
AST.Define version = defines.get("version");
if (version != null) {
if (!version.value.matches("\\d+\\.\\d+\\.\\d+")) {
error(
version,
"Define 'version' must be valid semantic versioning without pre-release identifier and"
+ " build metadata");
}
} else {
error("Missing required define '#version: \"\"'");
}
}
private void checkCategories() {
for (AST.Category category : ast.getCategories()) {
if (category.assets.isEmpty() && category.meta.isEmpty()) {
LOGGER.warning(
category.name,
String.format("Category '%s' contains no assets or metadata", category.name.id));
}
}
}
private void checkMetas() {
// Collect meta info from categories
Map<String, List<AST.Meta>> categoryMetas = new HashMap<>();
for (var category : ast.getCategories()) {
if (!categoryMetas.containsKey(category.name.id)) {
categoryMetas.put(category.name.id, new ArrayList<>());
}
categoryMetas.get(category.name.id).addAll(category.meta);
}
// Check meta info for categories
for (var metas : categoryMetas.values()) {
checkMeta(metas);
}
// Check meta info for assets and attack steps
for (var category : ast.getCategories()) {
for (var asset : category.assets) {
checkMeta(asset.meta);
for (var attackStep : asset.attackSteps) {
checkMeta(attackStep.meta);
}
}
}
// Check meta info for associations
for (var association : ast.getAssociations()) {
checkMeta(association.meta);
}
}
private void checkMeta(List<AST.Meta> lst) {
Map<String, AST.Meta> metas = new HashMap<>();
for (var meta : lst) {
if (!metas.containsKey(meta.type.id)) {
metas.put(meta.type.id, meta);
} else {
var prevDef = metas.get(meta.type.id);
error(
meta,
String.format(
"Metadata %s previously defined at %s", meta.type.id, prevDef.posString()));
}
}
}
private void checkAssets() {
for (AST.Category category : ast.getCategories()) {
for (AST.Asset asset : category.assets) {
if (assets.containsKey(asset.name.id)) {
AST.Asset prevDef = assets.get(asset.name.id);
error(
asset.name,
String.format(
"Asset '%s' previously defined at %s", asset.name.id, prevDef.name.posString()));
} else {
assets.put(asset.name.id, asset);
}
}
}
}
private void checkExtends() throws CompilerException {
boolean err = false;
for (AST.Asset asset : assets.values()) {
if (asset.parent.isPresent()) {
if (getAsset(asset.parent.get()) == null) {
err = true;
}
}
}
if (err) {
throw exception();
}
}
private void checkParents() throws CompilerException {
boolean err = false;
for (AST.Asset asset : assets.values()) {
if (asset.parent.isPresent()) {
Set<String> parents = new LinkedHashSet<>();
AST.Asset parent = asset;
do {
if (!parents.add(parent.name.id)) {
StringBuilder sb = new StringBuilder();
for (String parentName : parents) {
sb.append(parentName);
sb.append(" -> ");
}
sb.append(parent.name.id);
error(
asset.name,
String.format("Asset '%s' extends in loop '%s'", asset.name.id, sb.toString()));
err = true;
break;
}
parent = getAsset(parent.parent.get());
} while (parent.parent.isPresent());
}
}
if (err) {
throw exception();
}
}
private void checkAbstract() {
for (AST.Asset parent : assets.values()) {
if (parent.isAbstract) {
boolean found = false;
for (AST.Asset extendee : assets.values()) {
if (extendee.parent.isPresent() && extendee.parent.get().id.equals(parent.name.id)) {
found = true;
break;
}
}
if (!found) {
LOGGER.warning(
parent.name,
String.format("Asset '%s' is abstract but never extended to", parent.name.id));
}
}
}
}
private void checkSteps() {
for (AST.Asset asset : assets.values()) {
Scope<AST.AttackStep> scope = new Scope<>();
steps.put(asset.name.id, scope);
readSteps(scope, asset);
}
}
private void checkCIA() {
for (var asset : assets.values()) {
for (var attackStep : asset.attackSteps) {
if (attackStep.cia.isPresent()) {
if (attackStep.type == AST.AttackStepType.DEFENSE
|| attackStep.type == AST.AttackStepType.EXIST
|| attackStep.type == AST.AttackStepType.NOTEXIST) {
error(attackStep.name, "Defenses cannot have CIA classifications");
}
var cias = new HashSet<AST.CIA>();
for (var cia : attackStep.cia.get()) {
if (cias.contains(cia)) {
LOGGER.warning(
attackStep.name,
String.format(
"Attack step %s.%s contains duplicate classification {%s}",
asset.name.id, attackStep.name.id, cia));
} else {
cias.add(cia);
}
}
}
}
}
}
private void checkTTC() {
for (AST.Asset asset : assets.values()) {
for (AST.AttackStep attackStep : asset.attackSteps) {
if (attackStep.ttc.isPresent()) {
AST.TTCExpr ttc = attackStep.ttc.get();
if (attackStep.type == AST.AttackStepType.DEFENSE) {
if (!(ttc instanceof AST.TTCFuncExpr)) {
error(
attackStep,
String.format(
"Defense %s.%s may not have advanced TTC expressions",
asset.name.id, attackStep.name.id));
} else {
AST.TTCFuncExpr func = (AST.TTCFuncExpr) ttc;
switch (func.name.id) {
case "Enabled":
case "Disabled":
case "Bernoulli":
try {
Distributions.validate(func.name.id, func.params);
} catch (CompilerException e) {
error(func, e.getMessage());
}
break;
default:
error(
attackStep,
String.format(
"Defense %s.%s may only have 'Enabled', 'Disabled', or 'Bernoulli(p)' as"
+ " TTC",
asset.name.id, attackStep.name.id));
}
}
} else if (attackStep.type == AST.AttackStepType.ALL
|| attackStep.type == AST.AttackStepType.ANY) {
checkTTCExpr(attackStep.ttc.get());
}
}
}
}
}
private void checkTTCExpr(AST.TTCExpr expr) {
checkTTCExpr(expr, false);
}
private void checkTTCExpr(AST.TTCExpr expr, boolean isSubDivExp) {
if (expr instanceof AST.TTCBinaryExpr) {
isSubDivExp =
expr instanceof AST.TTCSubExpr
|| expr instanceof AST.TTCDivExpr
|| expr instanceof AST.TTCPowExpr;
checkTTCExpr(((AST.TTCBinaryExpr) expr).lhs, isSubDivExp);
checkTTCExpr(((AST.TTCBinaryExpr) expr).rhs, isSubDivExp);
} else if (expr instanceof AST.TTCFuncExpr) {
AST.TTCFuncExpr func = (AST.TTCFuncExpr) expr;
if (func.name.id.equals("Enabled") || func.name.id.equals("Disabled")) {
error(
expr,
"Distributions 'Enabled' or 'Disabled' may not be used as TTC values in '&' and '|'"
+ " attack steps");
} else {
if (isSubDivExp && Arrays.asList("Bernoulli", "EasyAndUncertain").contains(func.name.id)) {
error(
expr,
String.format(
"TTC distribution '%s' is not available in subtraction, division or exponential"
+ " expressions.",
func.name.id));
}
try {
Distributions.validate(func.name.id, func.params);
} catch (CompilerException e) {
error(func, e.getMessage());
}
}
} else if (expr instanceof AST.TTCNumExpr) {
// always ok
} else {
error(expr, String.format("Unexpected expression '%s'", expr.toString()));
System.exit(1);
}
}
/**
* Retrieves a list of an assets parents (including itself). The oldest parents will be first in
* the list. E.g. Alpha extends Bravo extends Charlie would return [Charlie, Bravo, Alpha] for
* asset Alpha.
*
* @param asset Child asset
* @return List of parents, oldest parent first in list
*/
private LinkedList<AST.Asset> getParents(AST.Asset asset) {
LinkedList<AST.Asset> lst = new LinkedList<>();
lst.addFirst(asset);
while (asset.parent.isPresent()) {
asset = getAsset(asset.parent.get());
lst.addFirst(asset);
}
return lst;
}
/**
* Populates a scope with attack steps of an asset and its parents. Checks semantic rules to make
* sure scope is correctly filled.
*
* @param scope Scope to populate
* @param asset Child asset
*/
private void readSteps(Scope<AST.AttackStep> scope, AST.Asset asset) {
List<AST.Asset> parents = getParents(asset);
for (AST.Asset parent : parents) {
if (parent.parent.isPresent()) {
scope = new Scope<>(scope);
steps.put(asset.name.id, scope);
}
for (AST.AttackStep attackStep : parent.attackSteps) {
AST.AttackStep prevDef = scope.look(attackStep.name.id);
if (prevDef == null) {
// Attack step is not defined in current scope
prevDef = scope.lookup(attackStep.name.id);
if (prevDef == null) {
// Attack step is not defined in any scope
if (attackStep.reaches.isEmpty() || !attackStep.reaches.get().inherits) {
// Attack step either doesn't reach anything or reaches with ->, OK
scope.add(attackStep.name.id, attackStep);
} else {
// Attack step reaches something with +> but doesn't exist previously, NOK
error(
attackStep.reaches.get(),
String.format(
"Cannot inherit attack step '%s' without previous definition",
attackStep.name.id));
}
} else {
// Attack step is previously defined in another scope
if (attackStep.type.equals(prevDef.type)) {
// Step is of same type as previous, OK
scope.add(attackStep.name.id, attackStep);
} else {
// Step is NOT of same type as previous, NOK
error(
attackStep.name,
String.format(
"Cannot override attack step '%s' previously defined at %s with different"
+ " type '%s' =/= '%s'",
attackStep.name.id, prevDef.name.posString(), attackStep.type, prevDef.type));
}
}
} else {
// Attack step is defined in this scope, NOK
error(
attackStep.name,
String.format(
"Attack step '%s' previously defined at %s",
attackStep.name.id, prevDef.name.posString()));
}
}
}
}
private void checkVariables() {
for (AST.Asset asset : assets.values()) {
Scope<AST.Variable> scope = new Scope<>();
assetVariables.put(asset.name.id, scope);
readVariables(scope, asset);
}
for (AST.Asset asset : assets.values()) {
var scope = assetVariables.get(asset.name.id);
for (var variable : scope.getSymbols().entrySet()) {
variableToAsset(asset, variable.getValue());
variableReferenceCount.put(variable.getValue(), 0);
}
}
}
/**
* Populate a scope with variable names from an asset and its parents
*
* @param scope
* @param asset
*/
private void readVariables(Scope<AST.Variable> scope, AST.Asset asset) {
List<AST.Asset> parents = getParents(asset);
for (AST.Asset parent : parents) {
if (parent.parent.isPresent()) {
scope = new Scope<>(scope);
assetVariables.put(asset.name.id, scope);
}
for (AST.Variable variable : parent.variables) {
addVariable(scope, variable);
}
}
}
private void checkFields() {
for (AST.Asset asset : assets.values()) {
Scope<AST.Association> scope = new Scope<>();
fields.put(asset.name.id, scope);
readFields(scope, asset);
}
}
/**
* Populates a scope with field names and associations from an asset and its parents.
*
* @param scope Scope to populate
* @param asset Child asset
*/
private void readFields(Scope<AST.Association> scope, AST.Asset asset) {
List<AST.Asset> parents = getParents(asset);
for (AST.Asset parent : parents) {
if (parent.parent.isPresent()) {
scope = new Scope<>(scope);
fields.put(asset.name.id, scope);
}
for (AST.Association assoc : ast.getAssociations()) {
if (assoc.leftAsset.id.equals(parent.name.id)) {
addField(scope, parent, asset, assoc.rightField, assoc);
}
// Association can be made from one asset to itself
if (assoc.rightAsset.id.equals(parent.name.id)) {
addField(scope, parent, asset, assoc.leftField, assoc);
}
}
}
}
private void addField(
Scope<AST.Association> scope,
AST.Asset parent,
AST.Asset asset,
AST.ID field,
AST.Association assoc) {
AST.Association prevDef = scope.lookdown(field.id);
if (prevDef == null) {
// Field not previously defined
AST.ID prevStep = hasStep(asset, field.id);
if (prevStep == null) {
scope.add(field.id, assoc);
} else {
// Field previously defined as attack step
error(
field,
String.format(
"Field '%s' previously defined as attack step at %s",
field.id, prevStep.posString()));
}
} else {
// Field previously defined
AST.ID prevField;
if (field.id.equals(prevDef.rightField.id)) {
prevField = prevDef.rightField;
} else {
prevField = prevDef.leftField;
}
error(
field,
String.format(
"Field %s.%s previously defined for asset at %s",
parent.name.id, field.id, prevField.posString()));
}
}
private void addVariable(Scope<AST.Variable> scope, AST.Variable variable) {
AST.Variable prevDef = scope.lookup(variable.name.id);
if (prevDef == null) {
variableReferenceCount.put(variable, 0);
scope.add(variable.name.id, variable);
} else {
error(
variable.name,
String.format(
"Variable '%s' previously defined at %s",
variable.name.id, prevDef.name.posString()));
}
}
/** Evaluates each expression reached by an attack step. */
private void checkReaches() throws CompilerException {
for (AST.Asset asset : assets.values()) {
for (AST.AttackStep attackStep : asset.attackSteps) {
if (attackStep.type == AST.AttackStepType.EXIST
|| attackStep.type == AST.AttackStepType.NOTEXIST) {
if (attackStep.ttc.isPresent()) {
error(
attackStep,
String.format("Attack step of type '%s' must not have TTC", attackStep.type));
continue;
}
if (attackStep.requires.isPresent()) {
// Requires (<-)
for (AST.Expr expr : attackStep.requires.get().requires) {
// Requires only have expressions that ends in assets/fields, not attack steps.
checkToAsset(asset, expr);
}
} else {
error(
attackStep,
String.format("Attack step of type '%s' must have require '<-'", attackStep.type));
continue;
}
} else if (attackStep.requires.isPresent()) {
error(
attackStep.requires.get(),
"Require '<-' may only be defined for attack step type exist 'E' or not-exist '!E'");
continue;
}
if (attackStep.reaches.isPresent()) {
for (AST.Expr expr : attackStep.reaches.get().reaches) {
checkToStep(asset, expr);
}
}
}
}
if (failed) {
throw exception();
}
}
private AST.AttackStep checkToStep(AST.Asset asset, AST.Expr expr) {
if (expr instanceof AST.IDExpr) {
AST.IDExpr step = (AST.IDExpr) expr;
AST.Asset target = asset;
AST.AttackStep attackStep = steps.get(target.name.id).lookup(step.id.id);
if (attackStep != null) {
return attackStep;
} else {
error(
step.id,
String.format(
"Attack step '%s' not defined for asset '%s'", step.id.id, target.name.id));
return null;
}
} else if (expr instanceof AST.StepExpr) {
AST.StepExpr step = (AST.StepExpr) expr;
AST.Asset target = checkToAsset(asset, step.lhs);
if (target != null) {
return checkToStep(target, step.rhs);
} else {
return null;
}
} else {
error(expr, "Last step is not attack step");
return null;
}
}
private AST.Asset checkToAsset(AST.Asset asset, AST.Expr expr) {
if (expr instanceof AST.StepExpr) {
return checkStepExpr(asset, (AST.StepExpr) expr);
} else if (expr instanceof AST.IDExpr) {
return checkIDExpr(asset, (AST.IDExpr) expr);
} else if (expr instanceof AST.IntersectionExpr
|| expr instanceof AST.UnionExpr
|| expr instanceof AST.DifferenceExpr) {
return checkSetExpr(asset, (AST.BinaryExpr) expr);
} else if (expr instanceof AST.TransitiveExpr) {
return checkTransitiveExpr(asset, (AST.TransitiveExpr) expr);
} else if (expr instanceof AST.SubTypeExpr) {
return checkSubTypeExpr(asset, (AST.SubTypeExpr) expr);
} else if (expr instanceof AST.CallExpr) {
return checkCallExpr(asset, (AST.CallExpr) expr);
} else {
error(expr, String.format("Unexpected expression '%s'", expr.toString()));
System.exit(1);
return null;
}
}
private AST.Asset checkStepExpr(AST.Asset asset, AST.StepExpr expr) {
AST.Asset leftTarget = checkToAsset(asset, expr.lhs);
if (leftTarget != null) {
AST.Asset rightTarget = checkToAsset(leftTarget, expr.rhs);
return rightTarget;
} else {
return null;
}
}
/**
* When evaluating a variable (variableToAsset()), a record must be kept to check for cyclic usage
* of variables.
*
* @param variable Variable evaluated
* @return True if variable is not being evaluated, false otherwise
*/
private boolean evalVariableBegin(AST.Variable variable) {
addVariableReference(variable);
if (currentVariables.add(variable)) {
return true;
} else {
StringBuilder sb = new StringBuilder();
for (var key : currentVariables) {
sb.append(key.name.id);
sb.append(" -> ");
}
sb.append(variable.name.id);
AST.Variable first = (AST.Variable) currentVariables.toArray()[0];
error(
first.name,
String.format("Variable '%s' contains cycle '%s'", first.name.id, sb.toString()));
return false;
}
}
private void evalVariableEnd(AST.Variable variable) {
currentVariables.remove(variable);
}
private AST.Asset variableToAsset(AST.Asset asset, AST.Variable variable) {
if (evalVariableBegin(variable)) {
AST.Asset res = checkToAsset(asset, variable.expr);
evalVariableEnd(variable);
return res;
} else {
return null;
}
}
private AST.Asset checkCallExpr(AST.Asset asset, AST.CallExpr expr) {
var scope = assetVariables.get(asset.name.id);
var variableScope = scope.getScopeFor(expr.id.id);
if (variableScope != null) {
var variable = variableScope.look(expr.id.id);
if (variable != null) {
return variableToAsset(asset, variable);
}
}
error(expr, String.format("Variable '%s' is not defined", expr.id.id));
return null;
}
private AST.Asset checkIDExpr(AST.Asset asset, AST.IDExpr expr) {
return getTarget(asset, expr.id);
}
private AST.Asset checkSetExpr(AST.Asset asset, AST.BinaryExpr expr) {
AST.Asset leftTarget = checkToAsset(asset, expr.lhs);
AST.Asset rightTarget = checkToAsset(asset, expr.rhs);
if (leftTarget == null || rightTarget == null) {
return null;
}
AST.Asset target = getLCA(leftTarget, rightTarget);
if (target != null) {
return target;
} else {
error(
expr,
String.format(
"Types '%s' and '%s' have no common ancestor",
leftTarget.name.id, rightTarget.name.id));
return null;
}
}
private AST.Asset checkTransitiveExpr(AST.Asset asset, AST.TransitiveExpr expr) {
AST.Asset res = checkToAsset(asset, expr.e);
if (res == null) {
return null;
}
if (isChild(res, asset)) {
return res;
} else {
error(
expr,
String.format("Previous asset '%s' is not of type '%s'", asset.name.id, res.name.id));
return null;
}
}
private AST.Asset checkSubTypeExpr(AST.Asset asset, AST.SubTypeExpr expr) {
AST.Asset target = checkToAsset(asset, expr.e);
if (target == null) {
return null;
}
AST.Asset type = getAsset(expr.subType);
if (type == null) {
return null;
}
if (isChild(target, type)) {
return type;
} else {
error(expr, String.format("Asset '%s' cannot be of type '%s'", target.name.id, type.name.id));
return null;
}
}
private AST.Asset getAsset(AST.ID name) {
if (assets.containsKey(name.id)) {
return assets.get(name.id);
} else {
error(name, String.format("Asset '%s' not defined", name.id));
return null;
}
}
private AST.ID hasStep(AST.Asset asset, String name) {
Scope<AST.AttackStep> scope = steps.get(asset.name.id);
AST.AttackStep attackStep = scope.lookdown(name);
if (attackStep != null) {
return attackStep.name;
} else {
return null;
}
}
private AST.Asset getTarget(AST.Asset asset, AST.ID name) {
Scope<AST.Association> scope = fields.get(asset.name.id);
AST.Association assoc = scope.lookdown(name.id);
if (assoc != null) {
addFieldReference(assoc, name);
if (assoc.leftField.id.equals(name.id)) {
return getAsset(assoc.leftAsset);
} else {
return getAsset(assoc.rightAsset);
}
} else {
String extra = "";
var varScope = assetVariables.get(asset.name.id).lookdown(name.id);
if (varScope != null) {
extra =
String.format(
", did you mean the variable '%s()' defined at %s", name.id, varScope.posString());
}
error(
name,
String.format("Field '%s' not defined for asset '%s'%s", name.id, asset.name.id, extra));
return null;
}
}
private boolean isChild(AST.Asset parent, AST.Asset child) {
if (parent.name.id.equals(child.name.id)) {
return true;
} else if (child.parent.isEmpty()) {
return false;
} else {
AST.Asset childParent = getAsset(child.parent.get());
return isChild(parent, childParent);
}
}
private AST.Asset getLCA(AST.Asset left, AST.Asset right) {
if (isChild(left, right)) {
return left;
} else if (isChild(right, left)) {
return right;
} else if (!left.parent.isPresent() && !right.parent.isPresent()) {
return null;
} else {
AST.Asset lparent = getAsset(left.parent.orElse(left.name));
AST.Asset rparent = getAsset(right.parent.orElse(right.name));
return getLCA(lparent, rparent);
}
}
private CompilerException exception() {
return new CompilerException("There were semantic errors");
}
private void error(String msg) {
failed = true;
LOGGER.error(msg);
}
private void error(Position pos, String msg) {
failed = true;
LOGGER.error(pos, msg);
}
}