chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.mm

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.h"

#import <Accessibility/Accessibility.h>

#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/platform/ax_utils_mac.h"
#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h"
#include "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h"
#include "ui/accessibility/platform/inspect/ax_property_node.h"

namespace ui {

// Template specialization of AXOptional<id>::ToString().
template <>
std::string AXOptional<id>::ToString() const {
  if (IsNotNull())
    return base::SysNSStringToUTF8([NSString stringWithFormat:@"%@", value_]);
  return StateToString();
}

#define INT_FAIL(property_node, msg)                              \
  LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
             << " to Int: " << msg;

#define INTARRAY_FAIL(property_node, msg)                         \
  LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
             << " to IntArray: " << msg;

#define NSRANGE_FAIL(property_node, msg)                          \
  LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
             << " to NSRange: " << msg;

#define UIELEMENT_FAIL(property_node, msg)                        \
  LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
             << " to UIElement: " << msg;

#define TEXTMARKER_FAIL(property_node, msg)                                    \
  LOG(ERROR) << "Failed to parse " << property_node.name_or_value              \
             << " to AXTextMarker: " << msg                                    \
             << ". Expected format: {anchor, offset, affinity}, where anchor " \
                "is :line_num, offset is integer, affinity is either down, "   \
                "up or none";

AXCallStatementInvoker::AXCallStatementInvoker(
    const AXTreeIndexerMac* indexer,
    std::map<std::string, id>* storage)
    : node(nullptr), indexer_(indexer), storage_(storage) {}

AXCallStatementInvoker::AXCallStatementInvoker(const id node,
                                               const AXTreeIndexerMac* indexer)
    : node(node), indexer_(indexer), storage_(nullptr) {}

AXOptionalNSObject AXCallStatementInvoker::Invoke(
    const AXPropertyNode& property_node,
    bool no_object_parse) const {
  // TODO(alexs): failing the tests when filters are incorrect is a good idea,
  // however crashing ax_dump tools on wrong input might be not. Figure out
  // a working solution that works nicely in both cases. Use LOG(ERROR) for now
  // as a console warning.

  // Executes a scripting statement coded in a given property node.
  // The statement represents a chainable sequence of attribute calls, where
  // each subsequent call is invoked on an object returned by a previous call.
  // For example, p.AXChildren[0].AXRole will unroll into a sequence of
  // `p.AXChildren`, `(p.AXChildren)[0]` and `((p.AXChildren)[0]).AXRole`.

  // Get an initial target to invoke an attribute for. First, check the storage
  // if it has an associated target for the property node, then query the tree
  // indexer if the property node refers to a DOM id or line index of
  // an accessible object. If the property node doesn't provide a target then
  // use the default one (if any, the default node is provided in case of
  // a tree dumping only, the scripts never have default target).
  id target = nil;

  // Case 1: try to get a target from the storage. The target may refer to
  // a variable which is kept in the storage. For example,
  // `text_leaf:= p.AXChildren[0]` will define `text_leaf` variable and put it
  // into the storage, and then the variable value will be extracted from
  // the storage for other instruction referring the variable, for example,
  // `text_leaf.AXRole`.
  if (storage_) {
    auto storage_iterator = storage_->find(property_node.name_or_value);
    if (storage_iterator != storage_->end()) {
      target = storage_iterator->second;
      if (!target)
        return AXOptionalNSObject(target);
    }
  }
  // Case 2: try to get target from the tree indexer. The target may refer to
  // an accessible element by DOM id or by a line number (:LINE_NUM format) in
  // a result accessible tree. The tree indexer keeps the mappings between
  // accessible elements and their DOM ids and line numbers.
  if (!target)
    target = indexer_->NodeBy(property_node.name_or_value);

  // Case 3: no target either indicates an error or default target (if
  // applicable) or the property node is an object or a scalar value (for
  // example, `0` in `AXChildren[0]` or [3, 4] integer array).
  if (!target) {
    // If default target is given, i.e. |node| is not null, then the target is
    // deemed and we use the default target. This case is about ax tree dumping
    // where a scripting instruction with no target are used. For example,
    // `AXRole` property filter means it is applied to all nodes and `AXRole`
    // attribute should be called for all nodes in the tree.
    if (IsDumpingTree()) {
      if (property_node.IsTarget()) {
        LOG(ERROR) << "Failed to parse '" << property_node.name_or_value
                   << "' target in '" << property_node.ToFlatString() << "'";
        return AXOptionalNSObject::Error();
      }
    } else if (no_object_parse) {
      return AXOptionalNSObject::NotApplicable();
    } else {
      // Object or scalar case.
      target = PropertyNodeToNSObject(property_node);
      if (!target) {
        LOG(ERROR) << "Failed to parse '" << property_node.ToFlatString()
                   << "' to NSObject";
        return AXOptionalNSObject::Error();
      }
    }
  }

  // If target is deemed, then start from the given property node. Otherwise the
  // given property node is a target, and its next property node is a
  // method/property to invoke.
  auto* current_node = &property_node;
  if (target) {
    current_node = property_node.next.get();
  } else {
    target = node;
  }

  // Invoke the call chain.
  while (current_node) {
    auto target_optional = InvokeFor(target, *current_node);
    // Result of the current step is state. Don't go any further.
    if (!target_optional.HasValue())
      return target_optional;

    target = *target_optional;
    current_node = current_node->next.get();
  }

  // Variable case: store the variable value in the storage.
  if (!property_node.key.empty())
    (*storage_)[property_node.key] = target;

  // When dumping tree, return NULL values as NotApplicable in order to
  // easily filter them out of the dump.
  return IsDumpingTree() ? AXOptionalNSObject::NotNullOrNotApplicable(target)
                         : AXOptionalNSObject(target);
}

AXOptionalNSObject AXCallStatementInvoker::InvokeFor(
    const id target,
    const AXPropertyNode& property_node) const {
  if (target == nil) {
    return AXOptionalNSObject::Error(
        "Cannot call '" + property_node.ToFlatString() + "' on null value");
  }

  if (AXElementWrapper::IsValidElement(target)) {
    return InvokeForAXElement(AXElementWrapper{target}, property_node);
  }

  if (IsAXTextMarkerRange(target)) {
    return InvokeForAXTextMarkerRange(target, property_node);
  }

  if ([target isKindOfClass:[NSArray class]]) {
    return InvokeForArray(target, property_node);
  }

  if ([target isKindOfClass:[NSDictionary class]]) {
    return InvokeForDictionary(target, property_node);
  }

  if ([target isKindOfClass:[AXCustomContent class]]) {
    return InvokeForAXCustomContent(target, property_node);
  }

  LOG(ERROR) << "Unexpected target type for " << property_node.ToFlatString();
  return AXOptionalNSObject::Error();
}

AXOptionalNSObject AXCallStatementInvoker::InvokeForAXCustomContent(
    const id target,
    const AXPropertyNode& property_node) const {
  AXCustomContent* content = target;

  if (property_node.name_or_value == "label") {
    return AXOptionalNSObject(content.label);
  }
  if (property_node.name_or_value == "value") {
    return AXOptionalNSObject(content.value);
  }

  return AXOptionalNSObject::Error(
      "Unrecognized '" + property_node.name_or_value +
      "' attribute called on AXCustomContent object.");
}

AXOptionalNSObject AXCallStatementInvoker::InvokeForAXElement(
    const AXElementWrapper& ax_element,
    const AXPropertyNode& property_node) const {
  // Actions.
  if (property_node.name_or_value == "AXActionNames") {
    return AXOptionalNSObject::NotNullOrNotApplicable(ax_element.ActionNames());
  }
  if (property_node.name_or_value == "AXPerformAction") {
    AXOptionalNSObject param = ParamFrom(property_node);
    if (param.IsNotNull()) {
      ax_element.PerformAction(*param);
      return AXOptionalNSObject::Unsupported();
    }
    return AXOptionalNSObject::Error();
  }

  // Get or set attribute value if the attribute is supported.
  for (NSString* attribute : ax_element.AttributeNames()) {
    if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) {
      // Setter
      if (property_node.rvalue) {
        AXOptionalNSObject rvalue = Invoke(*property_node.rvalue);
        if (rvalue.IsNotNull()) {
          ax_element.SetAttributeValue(attribute, *rvalue);
          return {rvalue};
        }
        return rvalue;
      }
      // Getter. Make sure to expose null values in ax scripts.
      AXOptionalNSObject optional_value =
          ax_element.GetAttributeValue(attribute);
      return IsDumpingTree()
                 ? AXOptionalNSObject::NotNullOrNotApplicable(*optional_value)
                 : optional_value;
    }
  }

  // Parameterized attributes.
  for (NSString* attribute : ax_element.ParameterizedAttributeNames()) {
    if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) {
      AXOptionalNSObject param = ParamFrom(property_node);
      if (param.IsNotNull()) {
        return ax_element.GetParameterizedAttributeValue(attribute, *param);
      }
      return param;
    }
  }

  // Only invoke methods whose names start with "accessibility", or
  // "isAccessibility" that is specific to NSAccessibility or UIAccessibility
  // protocols if the object responds to a corresponding selector. Ignore all
  // other selectors the object may respond to because it exposes unwanted
  // NSAccessibility attributes listed in default filters as a side effect.
  //
  // Methods whose names start with "isAccessibility" returns a BOOL, so we
  // need to handle the returned value differently than methods whose return
  // types are id.
  if (base::StartsWith(property_node.name_or_value, "isAccessibility")) {
    std::optional<SEL> optional_arg_selector;
    std::string selector_string = property_node.name_or_value;
    // In some cases, we might want to pass a SEL as argument instead of an id.
    // When an argument is prefixed with "@SEL:", transform the string into a
    // valid SEL to pass to the main selector.
    if (property_node.arguments.size() == 1 &&
        base::StartsWith(property_node.arguments[0].name_or_value, "@SEL:")) {
      optional_arg_selector = NSSelectorFromString(base::SysUTF8ToNSString(
          property_node.arguments[0].name_or_value.substr(5)));
      selector_string += ":";
    }

    SEL selector =
        NSSelectorFromString(base::SysUTF8ToNSString(selector_string));
    if (!ax_element.RespondsToSelector(selector))
      return AXOptionalNSObject::Error();

    BOOL return_value =
        optional_arg_selector
            ? ax_element.Invoke<BOOL, SEL>(selector, *optional_arg_selector)
            : ax_element.Invoke<BOOL>(selector);
    return AXOptionalNSObject(@(return_value));
  }

  if (property_node.name_or_value == "setAccessibilityFocused")
    return InvokeSetAccessibilityFocused(ax_element, property_node);

  // accessibilityAttributeValue
  if (property_node.name_or_value == "accessibilityAttributeValue") {
    if (property_node.arguments.size() == 1) {
      return AXOptionalNSObject(ax_element.GetAttributeValue(
          base::SysUTF8ToNSString(property_node.arguments[0].name_or_value)));
    }
    // Parameterized accessibilityAttributeValue.
    if (property_node.arguments.size() == 2) {
      const std::string& attribute = property_node.arguments[0].name_or_value;
      AXOptionalNSObject param =
          ParamFrom(attribute, property_node.arguments[1]);
      if (!param.HasValue())
        return AXOptionalNSObject::Error();

      return AXOptionalNSObject(ax_element.GetParameterizedAttributeValue(
          base::SysUTF8ToNSString(attribute), *param));
    }
    return AXOptionalNSObject::Error();
  }

  if (base::StartsWith(property_node.name_or_value, "accessibility")) {
    if (property_node.arguments.size() == 1) {
      std::optional<id> optional_id =
          ax_element.PerformSelector(property_node.name_or_value,
                                     property_node.arguments[0].name_or_value);
      if (optional_id) {
        return AXOptionalNSObject(*optional_id);
      }
    }
    if (property_node.arguments.empty()) {
      auto optional_id =
          ax_element.PerformSelector(property_node.name_or_value);
      if (optional_id) {
        return AXOptionalNSObject(*optional_id);
      }
    }
  }

  // Unmatched attribute.
  // * We choose not to return an error when dumping the accessibility tree,
  // because during this process the same set of NSAccessibility attributes
  // listed in property filters are queried on all nodes and, naturally, not all
  // nodes support all attributes.
  // * We also explicitly choose not to return an error if the NSAccessibility
  // attribute is valid and is in the list of attributes that our tree formatter
  // supports, but is not exposed on a given node.
  if (IsDumpingTree() || IsValidAXAttribute(property_node.name_or_value)) {
    return AXOptionalNSObject::NotApplicable();
  }

  LOG(ERROR) << "Unrecognized '" << property_node.name_or_value
             << "' attribute called on AXElement in '"
             << property_node.ToFlatString() << "' statement";
  return AXOptionalNSObject::Error();
}

AXOptionalNSObject AXCallStatementInvoker::InvokeForAXTextMarkerRange(
    const id target,
    const AXPropertyNode& property_node) const {
  if (property_node.name_or_value == "anchor")
    return AXOptionalNSObject(AXTextMarkerRangeStart(target));

  if (property_node.name_or_value == "focus")
    return AXOptionalNSObject(AXTextMarkerRangeEnd(target));

  // Unmatched attribute. We choose not to return an error when dumping the
  // accessibility tree, because during this process the same set of
  // NSAccessibility attributes listed in property filters are queried on all
  // nodes and, naturally, not all nodes support all attributes.
  if (IsDumpingTree())
    return AXOptionalNSObject::Unsupported();

  LOG(ERROR) << "Unrecognized '" << property_node.name_or_value
             << "' attribute called on AXTextMarkerRange in '"
             << property_node.ToFlatString() << "' statement";
  return AXOptionalNSObject::Error();
}

AXOptionalNSObject AXCallStatementInvoker::InvokeForArray(
    const id target,
    const AXPropertyNode& property_node) const {
  if (property_node.name_or_value == "count") {
    if (property_node.arguments.size()) {
      LOG(ERROR) << "count attribute is called as a method";
      return AXOptionalNSObject::Error();
    }
    return AXOptionalNSObject([NSNumber numberWithInt:[target count]]);
  }

  if (property_node.name_or_value == "has") {
    for (NSString* attribute : target) {
      if (property_node.arguments[0].IsMatching(
              base::SysNSStringToUTF8(attribute))) {
        return AXOptionalNSObject(static_cast<id>(@"yes"));
      }
    }
    return AXOptionalNSObject(static_cast<id>(@"no"));
  }

  if (!property_node.IsArray() || property_node.arguments.size() != 1) {
    LOG(ERROR) << "Array operator[] is expected, got: "
               << property_node.ToString();
    return AXOptionalNSObject::Error();
  }

  std::optional<int> maybe_index = property_node.arguments[0].AsInt();
  if (!maybe_index || *maybe_index < 0) {
    LOG(ERROR) << "Wrong index for array operator[], got: "
               << property_node.arguments[0].ToString();
    return AXOptionalNSObject::Error();
  }

  if (static_cast<int>([target count]) <= *maybe_index) {
    LOG(ERROR) << "Out of range array operator[] index, got: "
               << property_node.arguments[0].ToString()
               << ", length: " << [target count];
    return AXOptionalNSObject::Error();
  }

  return AXOptionalNSObject(target[*maybe_index]);
}

AXOptionalNSObject AXCallStatementInvoker::InvokeForDictionary(
    const id target,
    const AXPropertyNode& property_node) const {
  if (property_node.arguments.size() > 0) {
    LOG(ERROR) << "dictionary key is expected, got: "
               << property_node.ToString();
    return AXOptionalNSObject::Error();
  }

  NSString* key = PropertyNodeToString(property_node);
  NSDictionary* dictionary = target;
  return AXOptionalNSObject::NotNullOrError(dictionary[key]);
}

AXOptionalNSObject AXCallStatementInvoker::InvokeSetAccessibilityFocused(
    const AXElementWrapper& ax_element,
    const AXPropertyNode& property_node) const {
  std::string selector_string = property_node.name_or_value + ":";
  if (property_node.arguments.size() != 1) {
    LOG(ERROR) << "Wrong arguments number for " << selector_string
               << ", got: " << property_node.arguments.size()
               << ", expected: 1";
    return AXOptionalNSObject::Error();
  }

  SEL selector = NSSelectorFromString(base::SysUTF8ToNSString(selector_string));
  if (!ax_element.RespondsToSelector(selector)) {
    LOG(ERROR) << "Target doesn't answer to " << selector_string << " selector";
    return AXOptionalNSObject::Error();
  }

  BOOL val = property_node.arguments[0].name_or_value == "FALSE" ? FALSE : TRUE;
  ax_element.Invoke<void, BOOL>(selector, val);
  return AXOptionalNSObject(nil);
}

AXOptionalNSObject AXCallStatementInvoker::ParamFrom(
    const AXPropertyNode& property_node) const {
  // NSAccessibility attributes always take a single parameter.
  if (property_node.arguments.size() != 1) {
    LOG(ERROR) << "Failed to parse '" << property_node.ToFlatString()
               << "': single parameter is expected";
    return AXOptionalNSObject::Error();
  }

  return ParamFrom(property_node.name_or_value, property_node.arguments[0]);
}

AXOptionalNSObject AXCallStatementInvoker::ParamFrom(
    const std::string& attribute,
    const AXPropertyNode& argument) const {
  // Nested attribute case: attempt to invoke an attribute for an argument node.
  AXOptionalNSObject subvalue = Invoke(argument, /* no_object_parse= */ true);
  if (!subvalue.IsNotApplicable()) {
    return subvalue;
  }

  // Otherwise parse argument node value.
  if (attribute == "AXLineForIndex" ||
      attribute == "AXTextMarkerForIndex") {  // Int
    return AXOptionalNSObject::NotNullOrError(PropertyNodeToInt(argument));
  }
  if (attribute == "AXPerformAction") {
    return AXOptionalNSObject::NotNullOrError(PropertyNodeToString(argument));
  }
  if (attribute == "AXCellForColumnAndRow") {  // IntArray
    return AXOptionalNSObject::NotNullOrError(PropertyNodeToIntArray(argument));
  }
  if (attribute ==
      "AXTextMarkerRangeForUnorderedTextMarkers") {  // TextMarkerArray
    return AXOptionalNSObject::NotNullOrError(
        PropertyNodeToTextMarkerArray(argument));
  }
  if (attribute == "AXAttributedStringForRange" ||
      attribute == "AXStringForRange") {  // NSRange
    return AXOptionalNSObject::NotNullOrError(PropertyNodeToRange(argument));
  }
  if (attribute == "AXIndexForChildUIElement" ||
      attribute == "AXTextMarkerRangeForUIElement") {  // UIElement
    return AXOptionalNSObject::NotNullOrError(
        PropertyNodeToUIElement(argument));
  }
  if (attribute == "AXIndexForTextMarker" ||
      attribute == "AXNextWordEndTextMarkerForTextMarker" ||
      attribute ==
          "AXPreviousWordStartTextMarkerForTextMarker") {  // TextMarker
    return AXOptionalNSObject::NotNullOrError(
        PropertyNodeToTextMarker(argument));
  }
  if (attribute == "AXAttributedStringForTextMarkerRange" ||
      attribute == "AXSelectedTextMarkerRangeAttribute" ||
      attribute == "AXStringForTextMarkerRange") {  // TextMarkerRange
    return AXOptionalNSObject::NotNullOrError(
        PropertyNodeToTextMarkerRange(argument));
  }

  return AXOptionalNSObject::NotApplicable();
}

id AXCallStatementInvoker::PropertyNodeToNSObject(
    const AXPropertyNode& property_node) const {
  // Integer array
  id value = PropertyNodeToIntArray(property_node, false);
  if (value)
    return value;

  // NSRange
  value = PropertyNodeToRange(property_node, false);
  if (value)
    return value;

  // TextMarker
  value = PropertyNodeToTextMarker(property_node, true);
  if (value)
    return value;

  // TextMarker array
  value = PropertyNodeToTextMarkerArray(property_node, false);
  if (value)
    return value;

  // TextMarkerRange
  return PropertyNodeToTextMarkerRange(property_node, false);
}

// NSNumber. Format: integer.
NSNumber* AXCallStatementInvoker::PropertyNodeToInt(
    const AXPropertyNode& intnode,
    bool log_failure) const {
  std::optional<int> param = intnode.AsInt();
  if (!param) {
    if (log_failure)
      INT_FAIL(intnode, "not a number")
    return nil;
  }
  return [NSNumber numberWithInt:*param];
}

NSString* AXCallStatementInvoker::PropertyNodeToString(
    const AXPropertyNode& strnode,
    bool log_failure) const {
  std::string str = strnode.AsString();
  return base::SysUTF8ToNSString(str);
}

// NSArray of two NSNumber. Format: [integer, integer].
NSArray* AXCallStatementInvoker::PropertyNodeToIntArray(
    const AXPropertyNode& arraynode,
    bool log_failure) const {
  if (!arraynode.IsArray()) {
    if (log_failure)
      INTARRAY_FAIL(arraynode, "not array")
    return nil;
  }

  NSMutableArray* array =
      [[NSMutableArray alloc] initWithCapacity:arraynode.arguments.size()];
  for (const auto& paramnode : arraynode.arguments) {
    std::optional<int> param = paramnode.AsInt();
    if (!param) {
      if (log_failure)
        INTARRAY_FAIL(arraynode, paramnode.name_or_value + " is not a number")
      return nil;
    }
    [array addObject:@(*param)];
  }
  return array;
}

// NSArray of AXTextMarker objects.
NSArray* AXCallStatementInvoker::PropertyNodeToTextMarkerArray(
    const AXPropertyNode& arraynode,
    bool log_failure) const {
  if (!arraynode.IsArray()) {
    if (log_failure)
      INTARRAY_FAIL(arraynode, "not array")
    return nil;
  }

  NSMutableArray* array =
      [[NSMutableArray alloc] initWithCapacity:arraynode.arguments.size()];
  for (const auto& paramnode : arraynode.arguments) {
    AXOptionalNSObject text_marker = Invoke(paramnode);
    if (!text_marker.IsNotNull()) {
      if (log_failure)
        INTARRAY_FAIL(arraynode,
                      paramnode.ToFlatString() + "is not a text marker")
      return nil;
    }
    [array addObject:(*text_marker)];
  }
  return array;
}

// NSRange. Format: {loc: integer, len: integer}.
NSValue* AXCallStatementInvoker::PropertyNodeToRange(
    const AXPropertyNode& dictnode,
    bool log_failure) const {
  if (!dictnode.IsDict()) {
    if (log_failure)
      NSRANGE_FAIL(dictnode, "dictionary is expected")
    return nil;
  }

  std::optional<int> loc = dictnode.FindIntKey("loc");
  if (!loc) {
    if (log_failure)
      NSRANGE_FAIL(dictnode, "no loc or loc is not a number")
    return nil;
  }

  std::optional<int> len = dictnode.FindIntKey("len");
  if (!len) {
    if (log_failure)
      NSRANGE_FAIL(dictnode, "no len or len is not a number")
    return nil;
  }

  return [NSValue valueWithRange:NSMakeRange(*loc, *len)];
}

// UIElement. Format: :line_num.
gfx::NativeViewAccessible AXCallStatementInvoker::PropertyNodeToUIElement(
    const AXPropertyNode& uielement_node,
    bool log_failure) const {
  gfx::NativeViewAccessible uielement =
      indexer_->NodeBy(uielement_node.name_or_value);
  if (!uielement) {
    if (log_failure)
      UIELEMENT_FAIL(uielement_node,
                     "no corresponding UIElement was found in the tree")
    return nil;
  }
  return uielement;
}

id AXCallStatementInvoker::DictionaryNodeToTextMarker(
    const AXPropertyNode& dictnode,
    bool log_failure) const {
  if (!dictnode.IsDict()) {
    if (log_failure)
      TEXTMARKER_FAIL(dictnode, "dictionary is expected")
    return nil;
  }
  if (dictnode.arguments.size() != 3) {
    if (log_failure)
      TEXTMARKER_FAIL(dictnode, "wrong number of dictionary elements")
    return nil;
  }

  AXPlatformNodeCocoa* anchor_cocoa = static_cast<AXPlatformNodeCocoa*>(
      indexer_->NodeBy(dictnode.arguments[0].name_or_value));
  if (!anchor_cocoa) {
    if (log_failure)
      TEXTMARKER_FAIL(dictnode, "1st argument: wrong anchor")
    return nil;
  }

  std::optional<int> offset = dictnode.arguments[1].AsInt();
  if (!offset) {
    if (log_failure)
      TEXTMARKER_FAIL(dictnode, "2nd argument: wrong offset")
    return nil;
  }

  ax::mojom::TextAffinity affinity;
  const std::string& affinity_str = dictnode.arguments[2].name_or_value;
  if (affinity_str == "none") {
    affinity = ax::mojom::TextAffinity::kNone;
  } else if (affinity_str == "down") {
    affinity = ax::mojom::TextAffinity::kDownstream;
  } else if (affinity_str == "up") {
    affinity = ax::mojom::TextAffinity::kUpstream;
  } else {
    if (log_failure)
      TEXTMARKER_FAIL(dictnode, "3rd argument: wrong affinity")
    return nil;
  }

  return AXTextMarkerFrom(anchor_cocoa, *offset, affinity);
}

id AXCallStatementInvoker::PropertyNodeToTextMarker(
    const AXPropertyNode& dictnode,
    bool log_failure) const {
  return DictionaryNodeToTextMarker(dictnode, log_failure);
}

id AXCallStatementInvoker::PropertyNodeToTextMarkerRange(
    const AXPropertyNode& rangenode,
    bool log_failure) const {
  if (!rangenode.IsDict()) {
    if (log_failure)
      TEXTMARKER_FAIL(rangenode, "dictionary is expected")
    return nil;
  }

  const AXPropertyNode* anchornode = rangenode.FindKey("anchor");
  if (!anchornode) {
    if (log_failure)
      TEXTMARKER_FAIL(rangenode, "no anchor")
    return nil;
  }

  id anchor_textmarker = DictionaryNodeToTextMarker(*anchornode);
  if (!anchor_textmarker) {
    if (log_failure)
      TEXTMARKER_FAIL(rangenode, "failed to parse anchor")
    return nil;
  }

  const AXPropertyNode* focusnode = rangenode.FindKey("focus");
  if (!focusnode) {
    if (log_failure)
      TEXTMARKER_FAIL(rangenode, "no focus")
    return nil;
  }

  id focus_textmarker = DictionaryNodeToTextMarker(*focusnode);
  if (!focus_textmarker) {
    if (log_failure)
      TEXTMARKER_FAIL(rangenode, "failed to parse focus")
    return nil;
  }

  return AXTextMarkerRangeFrom(anchor_textmarker, focus_textmarker);
}

}  // namespace ui