dart:html: Add Element.removeAttribute

- Improve `element.attributes.remove(name)` to compile to `element.removeAttribute(name)`.
- Improve `element.getNamespacedAttributes(namespace).remove(name)` to compile to
  `element.removeAttributeNS(namespace, name)`.
- Add removeAttribute[NS].
- Add hasAttriute[NS].
- Assert attribute names are not null to prevent conversion to "null"/"undefined".
- TODO: Assert values are not null to prevent conversion to "null"/"undefined".
- namespaceURI does not need checking since null and undefined map to 'no namespace'.

Fixes https://github.com/dart-lang/sdk/issues/35655

Change-Id: Ie5bee23c88e8fb92f9d46f29fdf4b7f3175a2aa3
Reviewed-on: https://dart-review.googlesource.com/c/90160
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Terry Lucas <terry@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72262c9..7639a25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,16 @@
 
 [35576]: https://github.com/dart-lang/sdk/issues/35576
 
+#### `dart:html`
+
+*   Added methods `Element.removeAttribute`, `Element.removeAttributeNS`,
+    `Element.hasAttribute` and `Element.hasAttributeNS`. (Issue [35655][]).
+*   Improved dart2js compilation of `element.attributes.remove(name)` to
+    generate `element.removeAttribute(name)`, so that there is no performance
+    reason to migrate to the above methods.
+
+[35655]: https://github.com/dart-lang/sdk/issues/35655
+
 *   Fixed a number of 'dart:html' P1 bugs:
 
     *   Fixed HTML API's with callback typedef to correctly convert Dart function to JS function.
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index 9457101..f0f0153 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -1397,22 +1397,22 @@
 }
 
 @NoInline()
-checkNum(value) {
+num checkNum(value) {
   if (value is! num) throw argumentErrorValue(value);
   return value;
 }
 
-checkInt(value) {
+int checkInt(value) {
   if (value is! int) throw argumentErrorValue(value);
   return value;
 }
 
-checkBool(value) {
+bool checkBool(value) {
   if (value is! bool) throw argumentErrorValue(value);
   return value;
 }
 
-checkString(value) {
+String checkString(value) {
   if (value is! String) throw argumentErrorValue(value);
   return value;
 }
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index b7b158c..247fe40 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -145,7 +145,6 @@
     FontFace fontFace, FontFace fontFaceAgain, FontFaceSet set);
 
 WorkerGlobalScope get _workerSelf => JS('WorkerGlobalScope', 'self');
-
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
@@ -12307,6 +12306,66 @@
     }
   }
 
+  @pragma('dart2js:tryInline')
+  String getAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    return _getAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  String getAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    // [namespaceURI] does not need protecting, both `null` and `undefined` map to `null`.
+    assert(name != null, 'Attribute name cannot be null');
+    return _getAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  bool hasAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    return _hasAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  bool hasAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    // [namespaceURI] does not need protecting, both `null` and `undefined` map to `null`.
+    assert(name != null, 'Attribute name cannot be null');
+    return _hasAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void removeAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    _removeAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void removeAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    _removeAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void setAttribute(String name, String value) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    // TODO(sra): assert(value != null, 'Attribute value cannot be null.');
+    _setAttribute(name, value);
+  }
+
+  @pragma('dart2js:tryInline')
+  void setAttributeNS(String namespaceURI, String name, String value) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    // TODO(sra): assert(value != null, 'Attribute value cannot be null.');
+    _setAttributeNS(namespaceURI, name, value);
+  }
+
   /**
    * List of the direct children of this element.
    *
@@ -13916,9 +13975,11 @@
 
   List<Animation> getAnimations() native;
 
-  String getAttribute(String name) native;
+  @JSName('getAttribute')
+  String _getAttribute(String name) native;
 
-  String getAttributeNS(String namespaceURI, String localName) native;
+  @JSName('getAttributeNS')
+  String _getAttributeNS(String namespaceURI, String localName) native;
 
   List<String> getAttributeNames() native;
 
@@ -14083,9 +14144,11 @@
   @JSName('scrollTo')
   void _scrollTo_3(num x, y) native;
 
-  void setAttribute(String name, String value) native;
+  @JSName('setAttribute')
+  void _setAttribute(String name, String value) native;
 
-  void setAttributeNS(String namespaceURI, String name, String value) native;
+  @JSName('setAttributeNS')
+  void _setAttributeNS(String namespaceURI, String name, String value) native;
 
   void setPointerCapture(int pointerId) native;
 
@@ -34119,11 +34182,8 @@
     _element.setAttribute(key, value);
   }
 
-  String remove(Object key) {
-    String value = _element.getAttribute(key);
-    _element._removeAttribute(key);
-    return value;
-  }
+  @pragma('dart2js:tryInline')
+  String remove(Object key) => key is String ? _remove(_element, key) : null;
 
   /**
    * The number of {key, value} pairs in the map.
@@ -34133,6 +34193,21 @@
   }
 
   bool _matches(_Attr node) => node._namespaceUri == null;
+
+  // Inline this because almost all call sites of [remove] do not use [value],
+  // and the annotations on the `getAttribute` call allow it to be removed.
+  @pragma('dart2js:tryInline')
+  static String _remove(Element element, String key) {
+    String value = JS(
+        // throws:null(1) is not accurate since [key] could be malformed, but
+        // [key] is checked again by `removeAttributeNS`.
+        'returns:String|Null;depends:all;effects:none;throws:null(1)',
+        '#.getAttribute(#)',
+        element,
+        key);
+    JS('', '#.removeAttribute(#)', element, key);
+    return value;
+  }
 }
 
 /**
@@ -34155,11 +34230,9 @@
     _element.setAttributeNS(_namespace, key, value);
   }
 
-  String remove(Object key) {
-    String value = this[key];
-    _element._removeAttributeNS(_namespace, key);
-    return value;
-  }
+  @pragma('dart2js:tryInline')
+  String remove(Object key) =>
+      key is String ? _remove(_namespace, _element, key) : null;
 
   /**
    * The number of {key, value} pairs in the map.
@@ -34169,6 +34242,23 @@
   }
 
   bool _matches(_Attr node) => node._namespaceUri == _namespace;
+
+  // Inline this because almost all call sites of [remove] do not use the
+  // returned [value], and the annotations on the `getAttributeNS` call allow it
+  // to be removed.
+  @pragma('dart2js:tryInline')
+  static String _remove(String namespace, Element element, String key) {
+    String value = JS(
+        // throws:null(1) is not accurate since [key] could be malformed, but
+        // [key] is checked again by `removeAttributeNS`.
+        'returns:String|Null;depends:all;effects:none;throws:null(1)',
+        '#.getAttributeNS(#, #)',
+        element,
+        namespace,
+        key);
+    JS('', '#.removeAttributeNS(#, #)', element, namespace, key);
+    return value;
+  }
 }
 
 /**
diff --git a/tools/dom/scripts/htmlrenamer.py b/tools/dom/scripts/htmlrenamer.py
index eade5b6..dc5b6c0 100644
--- a/tools/dom/scripts/htmlrenamer.py
+++ b/tools/dom/scripts/htmlrenamer.py
@@ -348,10 +348,14 @@
   'Element.insertAdjacentHTML',
   'Element.scrollIntoView',
   'Element.scrollIntoViewIfNeeded',
-  'Element.removeAttribute',
-  'Element.removeAttributeNS',
+  'Element.getAttribute',
+  'Element.getAttributeNS',
   'Element.hasAttribute',
   'Element.hasAttributeNS',
+  'Element.removeAttribute',
+  'Element.removeAttributeNS',
+  'Element.setAttribute',
+  'Element.setAttributeNS',
   'Element.innerHTML',
   'Element.querySelectorAll',
   # TODO(vsm): These have been converted from int to double in Chrome 36.
diff --git a/tools/dom/src/AttributeMap.dart b/tools/dom/src/AttributeMap.dart
index f6a482f..71579e5 100644
--- a/tools/dom/src/AttributeMap.dart
+++ b/tools/dom/src/AttributeMap.dart
@@ -107,11 +107,8 @@
     _element.setAttribute(key, value);
   }
 
-  String remove(Object key) {
-    String value = _element.getAttribute(key);
-    _element._removeAttribute(key);
-    return value;
-  }
+  @pragma('dart2js:tryInline')
+  String remove(Object key) => key is String ? _remove(_element, key) : null;
 
   /**
    * The number of {key, value} pairs in the map.
@@ -121,6 +118,21 @@
   }
 
   bool _matches(_Attr node) => node._namespaceUri == null;
+
+  // Inline this because almost all call sites of [remove] do not use [value],
+  // and the annotations on the `getAttribute` call allow it to be removed.
+  @pragma('dart2js:tryInline')
+  static String _remove(Element element, String key) {
+    String value = JS(
+        // throws:null(1) is not accurate since [key] could be malformed, but
+        // [key] is checked again by `removeAttributeNS`.
+        'returns:String|Null;depends:all;effects:none;throws:null(1)',
+        '#.getAttribute(#)',
+        element,
+        key);
+    JS('', '#.removeAttribute(#)', element, key);
+    return value;
+  }
 }
 
 /**
@@ -143,11 +155,9 @@
     _element.setAttributeNS(_namespace, key, value);
   }
 
-  String remove(Object key) {
-    String value = this[key];
-    _element._removeAttributeNS(_namespace, key);
-    return value;
-  }
+  @pragma('dart2js:tryInline')
+  String remove(Object key) =>
+      key is String ? _remove(_namespace, _element, key) : null;
 
   /**
    * The number of {key, value} pairs in the map.
@@ -157,6 +167,23 @@
   }
 
   bool _matches(_Attr node) => node._namespaceUri == _namespace;
+
+  // Inline this because almost all call sites of [remove] do not use the
+  // returned [value], and the annotations on the `getAttributeNS` call allow it
+  // to be removed.
+  @pragma('dart2js:tryInline')
+  static String _remove(String namespace, Element element, String key) {
+    String value = JS(
+        // throws:null(1) is not accurate since [key] could be malformed, but
+        // [key] is checked again by `removeAttributeNS`.
+        'returns:String|Null;depends:all;effects:none;throws:null(1)',
+        '#.getAttributeNS(#, #)',
+        element,
+        namespace,
+        key);
+    JS('', '#.removeAttributeNS(#, #)', element, namespace, key);
+    return value;
+  }
 }
 
 /**
diff --git a/tools/dom/templates/html/dart2js/html_dart2js.darttemplate b/tools/dom/templates/html/dart2js/html_dart2js.darttemplate
index 2a30e63..83a8aa6 100644
--- a/tools/dom/templates/html/dart2js/html_dart2js.darttemplate
+++ b/tools/dom/templates/html/dart2js/html_dart2js.darttemplate
@@ -162,4 +162,3 @@
     FontFace fontFace, FontFace fontFaceAgain, FontFaceSet set);
 
 WorkerGlobalScope get _workerSelf => JS('WorkerGlobalScope', 'self');
-
diff --git a/tools/dom/templates/html/impl/impl_Element.darttemplate b/tools/dom/templates/html/impl/impl_Element.darttemplate
index 0ff97f6..740bc1c 100644
--- a/tools/dom/templates/html/impl/impl_Element.darttemplate
+++ b/tools/dom/templates/html/impl/impl_Element.darttemplate
@@ -563,6 +563,66 @@
     }
   }
 
+  @pragma('dart2js:tryInline')
+  String getAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    return _getAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  String getAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    // [namespaceURI] does not need protecting, both `null` and `undefined` map to `null`.
+    assert(name != null, 'Attribute name cannot be null');
+    return _getAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  bool hasAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    return _hasAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  bool hasAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    // [namespaceURI] does not need protecting, both `null` and `undefined` map to `null`.
+    assert(name != null, 'Attribute name cannot be null');
+    return _hasAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void removeAttribute(String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    _removeAttribute(name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void removeAttributeNS(String namespaceURI, String name) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    _removeAttributeNS(namespaceURI, name);
+  }
+
+  @pragma('dart2js:tryInline')
+  void setAttribute(String name, String value) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    // TODO(sra): assert(value != null, 'Attribute value cannot be null.');
+    _setAttribute(name, value);
+  }
+
+  @pragma('dart2js:tryInline')
+  void setAttributeNS(String namespaceURI, String name, String value) {
+    // Protect [name] against string conversion to "null" or "undefined".
+    assert(name != null, 'Attribute name cannot be null');
+    // TODO(sra): assert(value != null, 'Attribute value cannot be null.');
+    _setAttributeNS(namespaceURI, name, value);
+  }
+
   /**
    * List of the direct children of this element.
    *