blob: 64cf8eb5253ba0571602e598cdbe62e3702d9de0 [file] [log] [blame]
package org.checkerframework.checker.units;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.checker.signature.qual.CanonicalName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.checker.units.qual.MixedUnits;
import org.checkerframework.checker.units.qual.Prefix;
import org.checkerframework.checker.units.qual.UnitsBottom;
import org.checkerframework.checker.units.qual.UnitsMultiple;
import org.checkerframework.checker.units.qual.UnknownUnits;
import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.qual.AnnotatedFor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeFormatter;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotationClassLoader;
import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator;
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
import org.checkerframework.framework.util.DefaultQualifierKindHierarchy;
import org.checkerframework.framework.util.QualifierKind;
import org.checkerframework.framework.util.QualifierKindHierarchy;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.InternalUtils;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeSystemError;
import org.checkerframework.javacutil.UserError;
import org.plumelib.reflection.Signatures;
/**
* Annotated type factory for the Units Checker.
*
* <p>Handles multiple names for the same unit, with different prefixes, e.g. @kg is the same
* as @g(Prefix.kilo).
*
* <p>Supports relations between units, e.g. if "m" is a variable of type "@m" and "s" is a variable
* of type "@s", the division "m/s" is automatically annotated as "mPERs", the correct unit for the
* result.
*/
public class UnitsAnnotatedTypeFactory extends BaseAnnotatedTypeFactory {
private static final Class<org.checkerframework.checker.units.qual.UnitsRelations>
unitsRelationsAnnoClass = org.checkerframework.checker.units.qual.UnitsRelations.class;
protected final AnnotationMirror mixedUnits =
AnnotationBuilder.fromClass(elements, MixedUnits.class);
protected final AnnotationMirror TOP = AnnotationBuilder.fromClass(elements, UnknownUnits.class);
protected final AnnotationMirror BOTTOM =
AnnotationBuilder.fromClass(elements, UnitsBottom.class);
/** The UnitsMultiple.prefix argument/element. */
private final ExecutableElement unitsMultiplePrefixElement =
TreeUtils.getMethod(UnitsMultiple.class, "prefix", 0, processingEnv);
/** The UnitsMultiple.quantity argument/element. */
private final ExecutableElement unitsMultipleQuantityElement =
TreeUtils.getMethod(UnitsMultiple.class, "quantity", 0, processingEnv);
/** The UnitsRelations.value argument/element. */
private final ExecutableElement unitsRelationsValueElement =
TreeUtils.getMethod(
org.checkerframework.checker.units.qual.UnitsRelations.class, "value", 0, processingEnv);
/**
* Map from canonical class name to the corresponding UnitsRelations instance. We use the string
* to prevent instantiating the UnitsRelations multiple times.
*/
private Map<@CanonicalName String, UnitsRelations> unitsRel;
/** Map from canonical name of external qualifiers, to their Class. */
private static final Map<@CanonicalName String, Class<? extends Annotation>> externalQualsMap =
new HashMap<>();
private static final Map<String, AnnotationMirror> aliasMap = new HashMap<>();
public UnitsAnnotatedTypeFactory(BaseTypeChecker checker) {
// use true to enable flow inference, false to disable it
super(checker, false);
this.postInit();
}
// In Units Checker, we always want to print out the Invisible Qualifiers (UnknownUnits), and to
// format the print out of qualifiers by removing Prefix.one
@Override
protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() {
return new UnitsAnnotatedTypeFormatter(checker);
}
// Converts all metric-prefixed units' alias annotations (eg @kg) into base unit annotations
// with prefix values (eg @g(Prefix.kilo))
@Override
public AnnotationMirror canonicalAnnotation(AnnotationMirror anno) {
// Get the name of the aliased annotation
String aname = AnnotationUtils.annotationName(anno);
// See if we already have a map from this aliased annotation to its corresponding base unit
// annotation
if (aliasMap.containsKey(aname)) {
// if so return it
return aliasMap.get(aname);
}
boolean built = false;
AnnotationMirror result = null;
// if not, look for the UnitsMultiple meta annotations of this aliased annotation
for (AnnotationMirror metaAnno : anno.getAnnotationType().asElement().getAnnotationMirrors()) {
// see if the meta annotation is UnitsMultiple
if (isUnitsMultiple(metaAnno)) {
// retrieve the Class of the base unit annotation
Name baseUnitAnnoClass =
AnnotationUtils.getElementValueClassName(metaAnno, unitsMultipleQuantityElement);
// retrieve the SI Prefix of the aliased annotation
Prefix prefix =
AnnotationUtils.getElementValueEnum(
metaAnno, unitsMultiplePrefixElement, Prefix.class, Prefix.one);
// Build a base unit annotation with the prefix applied
result =
UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(
processingEnv, baseUnitAnnoClass, prefix);
// TODO: assert that this annotation is a prefix multiple of a Unit that's in the supported
// type qualifiers list currently this breaks for externally loaded annotations if the order
// was an alias before a base annotation.
// assert isSupportedQualifier(result);
built = true;
break;
}
}
if (built) {
// aliases shouldn't have Prefix.one, but if it does then clean it up here
if (UnitsRelationsTools.getPrefix(result) == Prefix.one) {
result = removePrefix(result);
}
// add this to the alias map
aliasMap.put(aname, result);
return result;
}
return super.canonicalAnnotation(anno);
}
/**
* Returns a map from canonical class name to the corresponding UnitsRelations instance.
*
* @return a map from canonical class name to the corresponding UnitsRelations instance
*/
protected Map<@CanonicalName String, UnitsRelations> getUnitsRel() {
if (unitsRel == null) {
unitsRel = new HashMap<>();
// Always add the default units relations, for the standard units.
// Other code adds more relations.
unitsRel.put(
UnitsRelationsDefault.class.getCanonicalName(),
new UnitsRelationsDefault().init(processingEnv));
}
return unitsRel;
}
@Override
protected AnnotationClassLoader createAnnotationClassLoader() {
// Use the UnitsAnnotationClassLoader instead of the default one
return new UnitsAnnotationClassLoader(checker);
}
@Override
protected Set<Class<? extends Annotation>> createSupportedTypeQualifiers() {
// get all the loaded annotations
Set<Class<? extends Annotation>> qualSet = getBundledTypeQualifiers();
// load all the external units
loadAllExternalUnits();
// copy all loaded external Units to qual set
qualSet.addAll(externalQualsMap.values());
return qualSet;
}
private void loadAllExternalUnits() {
// load external individually named units
String qualNames = checker.getOption("units");
if (qualNames != null) {
for (String qualName : qualNames.split(",")) {
if (!Signatures.isBinaryName(qualName)) {
throw new UserError("Malformed qualifier name \"%s\" in -Aunits=%s", qualName, qualNames);
}
loadExternalUnit(qualName);
}
}
// load external directories of units
String qualDirectories = checker.getOption("unitsDirs");
if (qualDirectories != null) {
for (String directoryName : qualDirectories.split(":")) {
loadExternalDirectory(directoryName);
}
}
}
/**
* Loads and processes a single external units qualifier.
*
* @param annoName the name of a units qualifier
*/
private void loadExternalUnit(@BinaryName String annoName) {
// loadExternalAnnotationClass() returns null for alias units
Class<? extends Annotation> loadedClass = loader.loadExternalAnnotationClass(annoName);
if (loadedClass != null) {
addUnitToExternalQualMap(loadedClass);
}
}
/** Loads and processes the units qualifiers from a single external directory. */
private void loadExternalDirectory(String directoryName) {
Set<Class<? extends Annotation>> annoClassSet =
loader.loadExternalAnnotationClassesFromDirectory(directoryName);
for (Class<? extends Annotation> annoClass : annoClassSet) {
addUnitToExternalQualMap(annoClass);
}
}
/** Adds the annotation class to the external qualifier map if it is not an alias annotation. */
private void addUnitToExternalQualMap(final Class<? extends Annotation> annoClass) {
AnnotationMirror mirror =
UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(
processingEnv, annoClass.getCanonicalName());
// if it is not an aliased annotation, add to external quals map if it isn't already in map
if (!isAliasedAnnotation(mirror)) {
String unitClassName = annoClass.getCanonicalName();
if (!externalQualsMap.containsKey(unitClassName)) {
externalQualsMap.put(unitClassName, annoClass);
}
}
// if it is an aliased annotation
else {
// ensure it has a base unit
@CanonicalName Name baseUnitClass = getBaseUnitAnno(mirror);
if (baseUnitClass != null) {
// if the base unit isn't already added, add that first
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/658
@DotSeparatedIdentifiers String baseUnitClassName = baseUnitClass.toString();
if (!externalQualsMap.containsKey(baseUnitClassName)) {
loadExternalUnit(baseUnitClassName);
}
// then add the aliased annotation to the alias map
// TODO: refactor so we can directly add to alias map, skipping the assert check in
// canonicalAnnotation.
canonicalAnnotation(mirror);
} else {
// error: somehow the aliased annotation has @UnitsMultiple meta annotation, but no
// base class defined in that meta annotation
// TODO: error abort
}
}
// process the units annotation and add its corresponding units relations class
addUnitsRelations(annoClass);
}
private boolean isAliasedAnnotation(AnnotationMirror anno) {
// loop through the meta annotations of the annotation, look for UnitsMultiple
for (AnnotationMirror metaAnno : anno.getAnnotationType().asElement().getAnnotationMirrors()) {
// see if the meta annotation is UnitsMultiple
if (isUnitsMultiple(metaAnno)) {
// TODO: does every alias have to have Prefix?
return true;
}
}
// if we are unable to find UnitsMultiple meta annotation, then this is not an Aliased
// Annotation
return false;
}
/**
* Return the name of the given annotation, if it is meta-annotated with UnitsMultiple; otherwise
* return null.
*
* @param anno the annotation to examine
* @return the annotation's name, if it is meta-annotated with UnitsMultiple; otherwise null
*/
private @Nullable @CanonicalName Name getBaseUnitAnno(AnnotationMirror anno) {
// loop through the meta annotations of the annotation, look for UnitsMultiple
for (AnnotationMirror metaAnno : anno.getAnnotationType().asElement().getAnnotationMirrors()) {
// see if the meta annotation is UnitsMultiple
if (isUnitsMultiple(metaAnno)) {
// TODO: does every alias have to have Prefix?
// Retrieve the base unit annotation.
Name baseUnitAnnoClass =
AnnotationUtils.getElementValueClassName(metaAnno, unitsMultipleQuantityElement);
return baseUnitAnnoClass;
}
}
return null;
}
/**
* Returns true if {@code metaAnno} is {@link UnitsMultiple}.
*
* @param metaAnno an annotation mirror
* @return true if {@code metaAnno} is {@link UnitsMultiple}
*/
private boolean isUnitsMultiple(AnnotationMirror metaAnno) {
return areSameByClass(metaAnno, UnitsMultiple.class);
}
/**
* Look for an @UnitsRelations annotation on the qualifier and add it to the list of
* UnitsRelations.
*
* @param qual the qualifier to investigate
*/
private void addUnitsRelations(Class<? extends Annotation> qual) {
AnnotationMirror am = AnnotationBuilder.fromClass(elements, qual);
for (AnnotationMirror ama : am.getAnnotationType().asElement().getAnnotationMirrors()) {
if (areSameByClass(ama, unitsRelationsAnnoClass)) {
String theclassname =
AnnotationUtils.getElementValueClassName(ama, unitsRelationsValueElement).toString();
if (!Signatures.isClassGetName(theclassname)) {
throw new UserError(
"Malformed class name \"%s\" should be in ClassGetName format in annotation %s",
theclassname, ama);
}
Class<?> valueElement;
try {
ClassLoader classLoader = InternalUtils.getClassLoaderForClass(AnnotationUtils.class);
valueElement = Class.forName(theclassname, true, classLoader);
} catch (ClassNotFoundException e) {
String msg =
String.format(
"Could not load class '%s' for field 'value' in annotation %s",
theclassname, ama);
throw new UserError(msg, e);
}
Class<? extends UnitsRelations> unitsRelationsClass;
try {
unitsRelationsClass = valueElement.asSubclass(UnitsRelations.class);
} catch (ClassCastException ex) {
throw new UserError(
"Invalid @UnitsRelations meta-annotation found in %s. "
+ "@UnitsRelations value %s is not a subclass of "
+ "org.checkerframework.checker.units.UnitsRelations.",
qual, ama);
}
String classname = unitsRelationsClass.getCanonicalName();
if (!getUnitsRel().containsKey(classname)) {
try {
unitsRel.put(
classname,
unitsRelationsClass.getDeclaredConstructor().newInstance().init(processingEnv));
} catch (Throwable e) {
throw new BugInCF("Throwable when instantiating UnitsRelations", e);
}
}
}
}
}
@Override
public TreeAnnotator createTreeAnnotator() {
// Don't call super.createTreeAnnotator because it includes PropagationTreeAnnotator which
// is incorrect.
return new ListTreeAnnotator(
new UnitsPropagationTreeAnnotator(this),
new LiteralTreeAnnotator(this).addStandardLiteralQualifiers(),
new UnitsTreeAnnotator(this));
}
private static class UnitsPropagationTreeAnnotator extends PropagationTreeAnnotator {
public UnitsPropagationTreeAnnotator(AnnotatedTypeFactory atypeFactory) {
super(atypeFactory);
}
// Handled completely by UnitsTreeAnnotator
@Override
public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) {
return null;
}
// Handled completely by UnitsTreeAnnotator
@Override
public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) {
return null;
}
}
/** A class for adding annotations based on tree. */
private class UnitsTreeAnnotator extends TreeAnnotator {
UnitsTreeAnnotator(UnitsAnnotatedTypeFactory atypeFactory) {
super(atypeFactory);
}
@Override
public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) {
AnnotatedTypeMirror lht = getAnnotatedType(node.getLeftOperand());
AnnotatedTypeMirror rht = getAnnotatedType(node.getRightOperand());
Tree.Kind kind = node.getKind();
// Remove Prefix.one
if (UnitsRelationsTools.getPrefix(lht) == Prefix.one) {
lht = UnitsRelationsTools.removePrefix(elements, lht);
}
if (UnitsRelationsTools.getPrefix(rht) == Prefix.one) {
rht = UnitsRelationsTools.removePrefix(elements, rht);
}
AnnotationMirror bestres = null;
for (UnitsRelations ur : getUnitsRel().values()) {
AnnotationMirror res = useUnitsRelation(kind, ur, lht, rht);
if (bestres != null && res != null && !bestres.equals(res)) {
checker.message(
Kind.WARNING,
"UnitsRelation mismatch, taking neither! Previous: "
+ bestres
+ " and current: "
+ res);
return null; // super.visitBinary(node, type);
}
if (res != null) {
bestres = res;
}
}
if (bestres != null) {
type.replaceAnnotation(bestres);
} else {
// If none of the units relations classes could resolve the units, then apply default rules
switch (kind) {
case MINUS:
case PLUS:
if (lht.getAnnotations().equals(rht.getAnnotations())) {
// The sum or difference has the same units as both operands.
type.replaceAnnotations(lht.getAnnotations());
} else {
// otherwise it results in mixed
type.replaceAnnotation(mixedUnits);
}
break;
case DIVIDE:
if (lht.getAnnotations().equals(rht.getAnnotations())) {
// If the units of the division match, return TOP
type.replaceAnnotation(TOP);
} else if (UnitsRelationsTools.hasNoUnits(rht)) {
// any unit divided by a scalar keeps that unit
type.replaceAnnotations(lht.getAnnotations());
} else if (UnitsRelationsTools.hasNoUnits(lht)) {
// scalar divided by any unit returns mixed
type.replaceAnnotation(mixedUnits);
} else {
// else it is a division of two units that have no defined relations
// from a relations class
// return mixed
type.replaceAnnotation(mixedUnits);
}
break;
case MULTIPLY:
if (UnitsRelationsTools.hasNoUnits(lht)) {
// any unit multiplied by a scalar keeps the unit
type.replaceAnnotations(rht.getAnnotations());
} else if (UnitsRelationsTools.hasNoUnits(rht)) {
// any scalar multiplied by a unit becomes the unit
type.replaceAnnotations(lht.getAnnotations());
} else {
// else it is a multiplication of two units that have no defined
// relations from a relations class
// return mixed
type.replaceAnnotation(mixedUnits);
}
break;
case REMAINDER:
// in modulo operation, it always returns the left unit regardless of what
// it is (unknown, or some unit)
type.replaceAnnotations(lht.getAnnotations());
break;
default:
// Placeholders for unhandled binary operations
// Do nothing
}
}
return null;
}
@Override
public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) {
ExpressionTree var = node.getVariable();
AnnotatedTypeMirror varType = getAnnotatedType(var);
type.replaceAnnotations(varType.getAnnotations());
return null;
}
private AnnotationMirror useUnitsRelation(
Tree.Kind kind, UnitsRelations ur, AnnotatedTypeMirror lht, AnnotatedTypeMirror rht) {
AnnotationMirror res = null;
if (ur != null) {
switch (kind) {
case DIVIDE:
res = ur.division(lht, rht);
break;
case MULTIPLY:
res = ur.multiplication(lht, rht);
break;
default:
// Do nothing
}
}
return res;
}
}
/** Set the Bottom qualifier as the bottom of the hierarchy. */
@Override
public QualifierHierarchy createQualifierHierarchy() {
return new UnitsQualifierHierarchy();
}
/** Qualifier Hierarchy for the Units Checker. */
@AnnotatedFor("nullness")
protected class UnitsQualifierHierarchy extends MostlyNoElementQualifierHierarchy {
/** Constructor. */
public UnitsQualifierHierarchy() {
super(UnitsAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), elements);
}
@Override
protected QualifierKindHierarchy createQualifierKindHierarchy(
@UnderInitialization UnitsQualifierHierarchy this,
Collection<Class<? extends Annotation>> qualifierClasses) {
return new UnitsQualifierKindHierarchy(qualifierClasses, elements);
}
@Override
protected boolean isSubtypeWithElements(
AnnotationMirror subAnno,
QualifierKind subKind,
AnnotationMirror superAnno,
QualifierKind superKind) {
return AnnotationUtils.areSame(subAnno, superAnno);
}
@Override
protected AnnotationMirror leastUpperBoundWithElements(
AnnotationMirror a1,
QualifierKind qualifierKind1,
AnnotationMirror a2,
QualifierKind qualifierKind2,
QualifierKind lubKind) {
if (qualifierKind1.isBottom()) {
return a2;
} else if (qualifierKind2.isBottom()) {
return a1;
} else if (qualifierKind1 == qualifierKind2) {
if (AnnotationUtils.areSame(a1, a2)) {
return a1;
} else {
@SuppressWarnings({
"nullness:assignment" // Every qualifier kind is a
// key in directSuperQualifierMap.
})
@NonNull AnnotationMirror lub =
((UnitsQualifierKindHierarchy) qualifierKindHierarchy)
.directSuperQualifierMap.get(qualifierKind1);
return lub;
}
}
throw new BugInCF("Unexpected QualifierKinds: %s %s", qualifierKind1, qualifierKind2);
}
@Override
protected AnnotationMirror greatestLowerBoundWithElements(
AnnotationMirror a1,
QualifierKind qualifierKind1,
AnnotationMirror a2,
QualifierKind qualifierKind2,
QualifierKind glbKind) {
return UnitsAnnotatedTypeFactory.this.BOTTOM;
}
}
/** UnitsQualifierKindHierarchy. */
@AnnotatedFor("nullness")
protected static class UnitsQualifierKindHierarchy extends DefaultQualifierKindHierarchy {
/**
* Mapping from QualifierKind to an AnnotationMirror that represents its direct super qualifier.
* Every qualifier kind maps to a nonnull AnnotationMirror.
*/
private final Map<QualifierKind, AnnotationMirror> directSuperQualifierMap;
/**
* Creates a UnitsQualifierKindHierarchy.
*
* @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy
* @param elements element utils
*/
public UnitsQualifierKindHierarchy(
Collection<Class<? extends Annotation>> qualifierClasses, Elements elements) {
super(qualifierClasses, UnitsBottom.class);
directSuperQualifierMap = createDirectSuperQualifierMap(elements);
}
/**
* Creates the direct super qualifier map.
*
* @param elements element utils
* @return the map
*/
@RequiresNonNull("this.qualifierKinds")
private Map<QualifierKind, AnnotationMirror> createDirectSuperQualifierMap(
@UnderInitialization UnitsQualifierKindHierarchy this, Elements elements) {
Map<QualifierKind, AnnotationMirror> directSuperType = new TreeMap<>();
for (QualifierKind qualifierKind : qualifierKinds) {
QualifierKind directSuperTypeKind = getDirectSuperQualifierKind(qualifierKind);
AnnotationMirror directSuperTypeAnno;
try {
directSuperTypeAnno = AnnotationBuilder.fromName(elements, directSuperTypeKind.getName());
} catch (BugInCF ex) {
throw new TypeSystemError("Unit annotations must have a default for all elements.");
}
if (directSuperTypeAnno == null) {
throw new TypeSystemError("Could not create AnnotationMirror: %s", directSuperTypeAnno);
}
directSuperType.put(qualifierKind, directSuperTypeAnno);
}
return directSuperType;
}
/**
* Get the direct super qualifier for the given qualifier kind.
*
* @param qualifierKind qualifier kind
* @return direct super qualifier kind
*/
private QualifierKind getDirectSuperQualifierKind(
@UnderInitialization UnitsQualifierKindHierarchy this, QualifierKind qualifierKind) {
if (qualifierKind.isTop()) {
return qualifierKind;
}
Set<QualifierKind> superQuals = new TreeSet<>(qualifierKind.getStrictSuperTypes());
while (superQuals.size() > 0) {
Set<QualifierKind> lowest = findLowestQualifiers(superQuals);
if (lowest.size() == 1) {
return lowest.iterator().next();
}
superQuals.removeAll(lowest);
}
throw new BugInCF("No direct super qualifier found for %s", qualifierKind);
}
}
private AnnotationMirror removePrefix(AnnotationMirror anno) {
return UnitsRelationsTools.removePrefix(elements, anno);
}
}