// Copyright 2014 The Bazel Authors. All rights reserved. // // 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. package com.google.devtools.build.lib.syntax; import com.google.common.collect.Streams; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.util.SpellChecker; import java.io.IOException; import java.util.Optional; /** Syntax node for a dot expression. e.g. obj.field, but not obj.method() */ public final class DotExpression extends Expression { private final Expression object; private final Identifier field; public DotExpression(Expression object, Identifier field) { this.object = object; this.field = field; } public Expression getObject() { return object; } public Identifier getField() { return field; } @Override public void prettyPrint(Appendable buffer) throws IOException { object.prettyPrint(buffer); buffer.append('.'); field.prettyPrint(buffer); } @Override Object doEval(Environment env) throws EvalException, InterruptedException { Object objValue = object.eval(env); String name = field.getName(); Object result = eval(objValue, name, getLocation(), env); return checkResult(objValue, result, name, getLocation()); } /** * Throws the correct error message if the result is null depending on the objValue. */ public static Object checkResult(Object objValue, Object result, String name, Location loc) throws EvalException { if (result != null) { return result; } throw getMissingFieldException(objValue, name, loc, "field"); } static EvalException getMissingFieldException( Object objValue, String name, Location loc, String accessName) { String suffix = ""; EvalException toSuppress = null; if (objValue instanceof ClassObject) { String customErrorMessage = ((ClassObject) objValue).getErrorMessageForUnknownField(name); if (customErrorMessage != null) { return new EvalException(loc, customErrorMessage); } try { suffix = SpellChecker.didYouMean(name, ((ClassObject) objValue).getFieldNames()); } catch (EvalException ee) { toSuppress = ee; } } else { suffix = SpellChecker.didYouMean( name, FuncallExpression.getStructFieldNames( objValue instanceof Class ? (Class) objValue : objValue.getClass())); } if (suffix.isEmpty() && hasMethod(objValue, name)) { // If looking up the field failed, then we know that this method must have struct_field=false suffix = ", however, a method of that name exists"; } EvalException ee = new EvalException( loc, String.format( "object of type '%s' has no %s '%s'%s", EvalUtils.getDataTypeName(objValue), accessName, name, suffix)); if (toSuppress != null) { ee.addSuppressed(toSuppress); } return ee; } /** Returns whether the given object has a method with the given name. */ static boolean hasMethod(Object obj, String name) { Class cls = obj instanceof Class ? (Class) obj : obj.getClass(); if (Runtime.getBuiltinRegistry().getFunctionNames(cls).contains(name)) { return true; } return FuncallExpression.getMethodNames(cls).contains(name); } /** * Returns the field of the given name of the struct objValue, or null if no such field exists. */ public static Object eval(Object objValue, String name, Location loc, Environment env) throws EvalException, InterruptedException { Iterable methods = objValue instanceof Class ? FuncallExpression.getMethods((Class) objValue, name) : FuncallExpression.getMethods(objValue.getClass(), name); if (methods != null) { Optional method = Streams.stream(methods).filter(MethodDescriptor::isStructField).findFirst(); if (method.isPresent() && method.get().isStructField()) { return method .get() .call( objValue, FuncallExpression.extraInterpreterArgs(method.get(), /* ast = */ null, loc, env) .toArray(), loc, env); } } if (objValue instanceof SkylarkClassObject) { try { return ((SkylarkClassObject) objValue).getValue(name); } catch (IllegalArgumentException ex) { throw new EvalException(loc, ex); } } else if (objValue instanceof ClassObject) { Object result = null; try { result = ((ClassObject) objValue).getValue(name); } catch (IllegalArgumentException ex) { throw new EvalException(loc, ex); } // ClassObjects may have fields that are annotated with @SkylarkCallable. // Since getValue() does not know about those, we cannot expect that result is a valid object. if (result != null) { result = SkylarkType.convertToSkylark(result, env); // If we access NestedSets using ClassObject.getValue() we won't know the generic type, // so we have to disable it. This should not happen. SkylarkType.checkTypeAllowedInSkylark(result, loc); return result; } } return null; } @Override public void accept(SyntaxTreeVisitor visitor) { visitor.visit(this); } @Override public Kind kind() { return Kind.DOT; } }