package org.fugazi.ql.type_checker;
import org.fugazi.ql.ast.expression.Expression;
import org.fugazi.ql.ast.expression.literal.ID;
import org.fugazi.ql.ast.form.Form;
import org.fugazi.ql.ast.form.form_data.QLFormDataStorage;
import org.fugazi.ql.ast.statement.ComputedQuestion;
import org.fugazi.ql.ast.statement.IfStatement;
import org.fugazi.ql.ast.statement.Question;
import org.fugazi.ql.ast.type.Type;
import org.fugazi.ql.type_checker.issue.ASTIssueHandler;
import org.fugazi.ql.type_checker.issue.ASTNodeIssue;
import org.fugazi.ql.type_checker.issue.error.DuplicateQuestionError;
import org.fugazi.ql.type_checker.issue.error.NonBoolConditionError;
import org.fugazi.ql.type_checker.issue.error.TypeMismatchError;
import org.fugazi.ql.type_checker.visitor.CyclicDependenciesVisitor;
import org.fugazi.ql.type_checker.visitor.TypeMismatchVisitor;
import org.fugazi.ql.type_checker.visitor.UndefinedQuestionsVisitor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class QLTypeChecker {
private final CyclicDependenciesVisitor cyclicDependenciesVisitor;
private final UndefinedQuestionsVisitor undefinedQuestionsVisitor;
private final TypeMismatchVisitor typeMismatchVisitor;
private final ASTIssueHandler astIssueHandler;
private final Form form;
private final QLFormDataStorage formData;
public QLTypeChecker(Form _form, QLFormDataStorage _formData) {
this.form = _form;
this.formData = _formData;
this.cyclicDependenciesVisitor = new CyclicDependenciesVisitor(this.formData);
this.undefinedQuestionsVisitor = new UndefinedQuestionsVisitor(this.formData);
this.typeMismatchVisitor = new TypeMismatchVisitor(this.formData);
this.astIssueHandler = new ASTIssueHandler();
}
/**
* =====================
* Private check methods
* =====================
*/
private void checkDuplicateLabels() {
List<Question> questions = this.formData.getAllQuestions();
List<String> labels = new ArrayList<>();
for (Question question : questions) {
String label = question.getLabel();
if (labels.contains(label)) {
this.astIssueHandler.registerNewWarning(question,
"Label defined multiple times! Possible confusion."
);
} else {
labels.add(label);
}
}
}
private void checkQuestionTypes() {
List<Question> questions = this.formData.getQuestions();
// this.formData.getAllQuestionTypes() cannot be reused since
// duplicate keys are simply overwritten there. The place to detect them
// is here.
Map<String, Type> questionTypes = new HashMap<>();
for (Question question : questions) {
if (this.wasQuestionDefinedWithDifferentType(questionTypes, question)) {
this.astIssueHandler.registerNewError(
new DuplicateQuestionError(),
question, "Question already defined with different type."
);
} else {
ID questionId = question.getIdentifier();
questionTypes.put(questionId.getName(), question.getType());
}
}
}
private void checkIfStatementConditionTypes() {
List<IfStatement> statements = this.formData.getIfStatements();
for (IfStatement ifStatement : statements) {
Expression expression = ifStatement.getCondition();
// check if condition of type bool
boolean conditionIsBool = expression.isExpressionOfTypeBool(this.formData);
if (!conditionIsBool) {
this.astIssueHandler.registerNewError(
new NonBoolConditionError(), ifStatement,
"Expression in if statement not of type bool."
);
}
}
}
private void checkAssignmentTypes() {
List<ComputedQuestion> questions = this.formData.getComputedQuestions();
for (ComputedQuestion question : questions) {
Type type = question.getType();
Expression computed = question.getComputedExpression();
// check if assigned types equal
boolean typesEqual = (type.equals(computed.getReturnedType(this.formData)));
if (!typesEqual) {
this.astIssueHandler.registerNewError(
new TypeMismatchError(), question,
"Attempted to assign type " + computed.getReturnedType(this.formData)
+ " to variable of type " + type.getClass() + "."
);
}
}
}
private void checkForUndefinedQuestions(Form _form) {
this.undefinedQuestionsVisitor.visitForm(_form);
}
private void checkForTypeMismatches(Form _form) {
this.typeMismatchVisitor.visitForm(_form);
}
private void checkForCyclicDependencies(Form _form) {
this.cyclicDependenciesVisitor.visitForm(_form);
}
/**
* =====================
* Helper check methods
* =====================
*/
private boolean wasQuestionDefinedWithDifferentType(
Map<String, Type> questionTypes, Question question
) {
ID questionId = question.getIdentifier();
Type earlierQuestionType = questionTypes.get(questionId.getName());
if ((earlierQuestionType != null)
&& (!earlierQuestionType.equals(question.getType()))) {
return true;
}
return false;
}
/**
* =====================
* Exposed global methods
* =====================
*/
public boolean checkForm() {
// clear errors and warnings
// (so that multiple checks can be performed on one instance)
this.clearErrorsAndWarnings();
// perform all checks that require storage
this.checkDuplicateLabels();
this.checkQuestionTypes();
this.checkIfStatementConditionTypes();
this.checkAssignmentTypes();
// perform all the checks that are done on the fly
this.checkForUndefinedQuestions(this.form);
this.checkForTypeMismatches(this.form);
this.checkForCyclicDependencies(this.form);
return this.isFormCorrect();
}
public boolean isFormCorrect() {
return !this.hasErrors();
}
public boolean hasErrors() {
return (this.astIssueHandler.hasErrors()
|| this.undefinedQuestionsVisitor.hasErrors()
|| this.typeMismatchVisitor.hasErrors()
|| this.cyclicDependenciesVisitor.hasErrors());
}
public List<ASTNodeIssue> getErrors() {
List<ASTNodeIssue> errors = this.astIssueHandler.getErrors();
errors.addAll(this.undefinedQuestionsVisitor.getErrors());
errors.addAll(this.typeMismatchVisitor.getErrors());
errors.addAll(this.cyclicDependenciesVisitor.getErrors());
return errors;
}
public List<ASTNodeIssue> getWarnings() {
List<ASTNodeIssue> warnings = this.astIssueHandler.getWarnings();
return warnings;
}
public void clearErrorsAndWarnings() {
this.astIssueHandler.clearErrorsAndWarnings();
this.undefinedQuestionsVisitor.clearErrorsAndWarnings();
this.typeMismatchVisitor.clearErrorsAndWarnings();
this.cyclicDependenciesVisitor.clearErrorsAndWarnings();
}
}