From d3c33d5821fedc99775b0710c5d1abaffb6b8ab8 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Mon, 20 Jul 2015 16:15:11 +0100
Subject: [PATCH] Issue #2506581 by alexpott, Wim Leers, Fabianx, xjm,
 joelpittet, Cottser, dawehner: Remove SafeMarkup::set() from
 Renderer::doRender

---
 core/includes/bootstrap.inc                   |  2 +-
 .../Ajax/AjaxResponseAttachmentsProcessor.php |  6 +-
 .../Ajax/CommandWithAttachedAssetsTrait.php   |  4 +-
 .../Core/Render/MainContent/AjaxRenderer.php  |  4 +-
 core/lib/Drupal/Core/Render/RenderCache.php   | 16 ++--
 core/lib/Drupal/Core/Render/Renderer.php      | 37 ++++----
 .../Drupal/Core/Render/RendererInterface.php  |  6 +-
 core/lib/Drupal/Core/Render/SafeString.php    | 84 +++++++++++++++++++
 .../StringTranslation/TranslationWrapper.php  |  4 +-
 .../Drupal/Core/Template/TwigEnvironment.php  |  9 +-
 .../lib/Drupal/Core/Utility/LinkGenerator.php |  8 ++
 .../block/src/Tests/BlockViewBuilderTest.php  | 10 +--
 core/modules/contact/contact.module           |  4 +-
 .../contextual/src/ContextualController.php   |  2 +-
 core/modules/editor/src/EditorController.php  |  2 +-
 core/modules/filter/filter.module             |  2 +-
 .../filter/src/Tests/FilterAPITest.php        |  8 +-
 .../quickedit/src/QuickEditController.php     |  6 +-
 .../src/Tests/Views/StyleSerializerTest.php   |  2 +-
 .../simpletest/src/AssertContentTrait.php     |  4 +-
 .../Tests/Common/RenderElementTypesTest.php   |  2 +-
 .../Tests/Entity/EntityTranslationTest.php    |  4 +-
 .../views/src/Plugin/views/PluginBase.php     |  2 +-
 .../views/src/Plugin/views/display/Feed.php   |  2 +-
 .../Plugin/views/field/FieldPluginBase.php    |  9 +-
 .../views/src/Plugin/views/row/OpmlFields.php |  2 +-
 .../Plugin/views/style/StylePluginBase.php    |  9 +-
 .../src/Tests/Handler/FieldCounterTest.php    | 12 +--
 .../src/Tests/Handler/FieldFieldTest.php      | 68 ++++++++-------
 .../views/src/Tests/QueryGroupByTest.php      | 16 ++--
 .../Core/Render/RendererBubblingTest.php      | 12 +--
 .../Core/Render/RendererPlaceholdersTest.php  | 34 ++++----
 .../Drupal/Tests/Core/Render/RendererTest.php | 13 +--
 33 files changed, 238 insertions(+), 167 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Render/SafeString.php

diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 7bfa55c3d286..5d4424abdb42 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -563,7 +563,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
 
     $new = array(
       'safe' => SafeMarkup::isSafe($message),
-      'message' => $message,
+      'message' => (string) $message,
     );
     if ($repeat || !in_array($new, $_SESSION['messages'][$type])) {
       $_SESSION['messages'][$type][] = $new;
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index 7d36b51c718e..bffe66619833 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -164,15 +164,15 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req
     $resource_commands = array();
     if ($css_assets) {
       $css_render_array = $this->cssCollectionRenderer->render($css_assets);
-      $resource_commands[] = new AddCssCommand($this->renderer->renderPlain($css_render_array));
+      $resource_commands[] = new AddCssCommand((string) $this->renderer->renderPlain($css_render_array));
     }
     if ($js_assets_header) {
       $js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
-      $resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
+      $resource_commands[] = new PrependCommand('head', (string) $this->renderer->renderPlain($js_header_render_array));
     }
     if ($js_assets_footer) {
       $js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
-      $resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
+      $resource_commands[] = new AppendCommand('body', (string) $this->renderer->renderPlain($js_footer_render_array));
     }
     foreach (array_reverse($resource_commands) as $resource_command) {
       $response->addCommand($resource_command, TRUE);
diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
index fe117e173083..d83db5779f2a 100644
--- a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
+++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
@@ -37,10 +37,10 @@ protected function getRenderedContent() {
     if (is_array($this->content)) {
       $html = \Drupal::service('renderer')->renderRoot($this->content);
       $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
-      return $html;
+      return (string) $html;
     }
     else {
-      return $this->content;
+      return (string) $this->content;
     }
   }
 
diff --git a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
index 31192f35f999..c54799db6cf3 100644
--- a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
@@ -64,7 +64,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
       }
     }
 
-    $html = $this->drupalRenderRoot($main_content);
+    $html = (string) $this->drupalRenderRoot($main_content);
     $response->setAttachments($main_content['#attached']);
 
     // The selector for the insert command is NULL as the new content will
@@ -72,7 +72,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // behavior can be changed with #ajax['method'].
     $response->addCommand(new InsertCommand(NULL, $html));
     $status_messages = array('#type' => 'status_messages');
-    $output = $this->drupalRenderRoot($status_messages);
+    $output = (string) $this->drupalRenderRoot($status_messages);
     if (!empty($output)) {
       $response->addCommand(new PrependCommand(NULL, $output));
     }
diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php
index 28549116e8be..880fca36f505 100644
--- a/core/lib/Drupal/Core/Render/RenderCache.php
+++ b/core/lib/Drupal/Core/Render/RenderCache.php
@@ -77,11 +77,6 @@ public function get(array $elements) {
       if (isset($cached_element['#cache_redirect'])) {
         return $this->get($cached_element);
       }
-      // Ensure that any safe properties are marked safe.
-      foreach ($cached_element['#safe_cache_properties'] as $cache_property) {
-        SafeMarkup::set($cached_element[$cache_property]);
-      }
-      unset($cached_element['#safe_cache_properties']);
       // Return the cached element.
       return $cached_element;
     }
@@ -333,7 +328,6 @@ public function getCacheableRenderArray(array $elements) {
         'tags' => $elements['#cache']['tags'],
         'max-age' => $elements['#cache']['max-age'],
       ],
-      '#safe_cache_properties' => []
     ];
 
     // Preserve cacheable items if specified. If we are preserving any cacheable
@@ -342,10 +336,10 @@ public function getCacheableRenderArray(array $elements) {
     // the cache entry size.
     if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
       $data['#cache_properties'] = $elements['#cache_properties'];
-      // Store whether any of the cache properties are safe strings.
+      // Ensure that any safe strings are a SafeString object.
       foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) {
-        if (isset($elements[$cache_property]) && !is_array($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
-          $data['#safe_cache_properties'][] = $cache_property;
+        if (isset($elements[$cache_property]) && is_scalar($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
+          $elements[$cache_property] = SafeString::create($elements[$cache_property]);
         }
       }
 
@@ -357,12 +351,14 @@ public function getCacheableRenderArray(array $elements) {
         $data['#markup'] = '';
         // Cache only cacheable children's markup.
         foreach ($cacheable_children as $key) {
-          $cacheable_items[$key] = ['#markup' => $cacheable_items[$key]['#markup']];
+          // We can assume that #markup is safe at this point.
+          $cacheable_items[$key] = ['#markup' => SafeString::create($cacheable_items[$key]['#markup'])];
         }
       }
       $data += $cacheable_items;
     }
 
+    $data['#markup'] = SafeString::create($data['#markup']);
     return $data;
   }
 
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index d68282278771..cbd9ca853d98 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
@@ -277,15 +278,9 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
         if ($is_root_call) {
           $this->replacePlaceholders($elements);
         }
-        // Mark the element markup as safe. If we have cached children, we need
-        // to mark them as safe too. The parent markup contains the child
-        // markup, so if the parent markup is safe, then the markup of the
-        // individual children must be safe as well.
-        $elements['#markup'] = SafeMarkup::set($elements['#markup']);
-        if (!empty($elements['#cache_properties'])) {
-          foreach (Element::children($cached_element) as $key) {
-            SafeMarkup::set($cached_element[$key]['#markup']);
-          }
+        // Mark the element markup as safe if is it a string.
+        if (is_string($elements['#markup'])) {
+          $elements['#markup'] = SafeString::create($elements['#markup']);
         }
         // The render cache item contains all the bubbleable rendering metadata
         // for the subtree.
@@ -410,7 +405,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $elements['#children'] = '';
     }
 
-    if (isset($elements['#markup'])) {
+    if (!empty($elements['#markup'])) {
       // @todo Decide how to support non-HTML in the render API in
       //   https://www.drupal.org/node/2501313.
       $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']);
@@ -452,7 +447,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       foreach ($children as $key) {
         $elements['#children'] .= $this->doRender($elements[$key]);
       }
-      $elements['#children'] = SafeMarkup::set($elements['#children']);
+      $elements['#children'] = SafeString::create($elements['#children']);
     }
 
     // If #theme is not implemented and the element has raw #markup as a
@@ -463,7 +458,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
-      $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
+      $elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']);
     }
 
     // Let the theme functions in #theme_wrappers add markup around the rendered
@@ -551,8 +546,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $context->bubble();
 
     $elements['#printed'] = TRUE;
-    $elements['#markup'] = SafeMarkup::set($elements['#markup']);
-    return $elements['#markup'];
+    return SafeString::create($elements['#markup']);
   }
 
   /**
@@ -710,17 +704,18 @@ public function addCacheableDependency(array &$elements, $dependency) {
    * Note: This method only filters if $string is not marked safe already. This
    * ensures that HTML intended for display is not filtered.
    *
-   * @param string $string
+   * @param string|\Drupal\Core\Render\SafeString $string
    *   A string.
    *
-   * @return string
-   *   The escaped string. If SafeMarkup::isSafe($string) returns TRUE, it won't
-   *   be escaped again.
+   * @return \Drupal\Core\Render\SafeString
+   *   The escaped string wrapped in a SafeString object. If
+   *   SafeMarkup::isSafe($string) returns TRUE, it won't be escaped again.
    */
   protected function xssFilterAdminIfUnsafe($string) {
-    // @todo https://www.drupal.org/node/2506581 replace with
-    //   SafeMarkup::isSafe() and Xss::filterAdmin().
-    return SafeMarkup::checkAdminXss($string);
+    if (!SafeMarkup::isSafe($string)) {
+      $string = Xss::filterAdmin($string);
+    }
+    return SafeString::create($string);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index af3bcdad3c16..155eec4dbadf 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -27,7 +27,7 @@ interface RendererInterface {
    * @param array $elements
    *   The structured array describing the data to be rendered.
    *
-   * @return string
+   * @return \Drupal\Component\Utility\SafeStringInterface
    *   The rendered HTML.
    *
    * @see ::render()
@@ -58,7 +58,7 @@ public function renderRoot(&$elements);
    * @param array $elements
    *   The structured array describing the data to be rendered.
    *
-   * @return string
+   * @return \Drupal\Component\Utility\SafeStringInterface
    *   The rendered HTML.
    *
    * @see ::renderRoot()
@@ -302,7 +302,7 @@ public function renderPlain(&$elements);
    *   (Internal use only.) Whether this is a recursive call or not. See
    *   ::renderRoot().
    *
-   * @return string
+   * @return \Drupal\Component\Utility\SafeStringInterface
    *   The rendered HTML.
    *
    * @throws \LogicException
diff --git a/core/lib/Drupal/Core/Render/SafeString.php b/core/lib/Drupal/Core/Render/SafeString.php
new file mode 100644
index 000000000000..170331e18ac2
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/SafeString.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\SafeString.
+ */
+
+namespace Drupal\Core\Render;
+
+use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\Unicode;
+
+/**
+ * Defines an object that passes safe strings through the render system.
+ *
+ * This object should only be constructed with a known safe string. If there is
+ * any risk that the string contains user-entered data that has not been
+ * filtered first, it must not be used.
+ *
+ * @internal
+ *   This object is marked as internal because it should only be used during
+ *   rendering. Currently, there is no use case for this object by contrib or
+ *   custom code.
+ *
+ * @see \Drupal\Core\Template\TwigExtension::escapeFilter
+ * @see \Twig_Markup
+ * @see \Drupal\Component\Utility\SafeMarkup
+ */
+class SafeString implements SafeStringInterface, \Countable {
+
+  /**
+   * The safe string.
+   *
+   * @var string
+   */
+  protected $string;
+
+  /**
+   * Creates a SafeString object if necessary.
+   *
+   * If $string is equal to a blank string then it is not necessary to create a
+   * SafeString object. If $string is an object that implements
+   * SafeStringInterface it is returned unchanged.
+   *
+   * @param mixed $string
+   *   The string to mark as safe. This value will be cast to a string.
+   *
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   A safe string.
+   */
+  public static function create($string) {
+    if ($string instanceof SafeStringInterface) {
+      return $string;
+    }
+    $string = (string) $string;
+    if ($string === '') {
+      return '';
+    }
+    $safe_string = new static();
+    $safe_string->string = $string;
+    return $safe_string;
+  }
+
+  /**
+   * Returns the string version of the SafeString object.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function __toString() {
+    return $this->string;
+  }
+
+  /**
+   * Returns the string length.
+   *
+   * @return int
+   *   The length of the string.
+   */
+  public function count() {
+    return Unicode::strlen($this->string);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
index e2c1a980d886..3a36db5b1bdb 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\StringTranslation;
 
+use Drupal\Component\Utility\SafeStringInterface;
+
 /**
  * Provides a class to wrap a translatable string.
  *
@@ -16,7 +18,7 @@
  *
  * @see \Drupal\Core\Annotation\Translation
  */
-class TranslationWrapper {
+class TranslationWrapper implements SafeStringInterface {
   use StringTranslationTrait;
 
   /**
diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php
index 0af48ceaec1c..346dcc24b098 100644
--- a/core/lib/Drupal/Core/Template/TwigEnvironment.php
+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\PhpStorage\PhpStorageFactory;
+use Drupal\Core\Render\SafeString;
 
 /**
  * A class that defines a Twig environment for Drupal.
@@ -194,17 +195,15 @@ public function getTemplateClass($name, $index = NULL) {
    * @param array $context
    *   An array of parameters to pass to the template.
    *
-   * @return string
-   *   The rendered inline template.
+   * @return \Drupal\Component\Utility\SafeStringInterface|string
+   *   The rendered inline template as a SafeString object.
    *
    * @see \Drupal\Core\Template\Loader\StringLoader::exists()
    */
   public function renderInline($template_string, array $context = array()) {
     // Prefix all inline templates with a special comment.
     $template_string = '{# inline_template_start #}' . $template_string;
-    // @todo replace with object implementating SafeStringInterface in
-    //   https://www.drupal.org/node/2506581.
-    return SafeMarkup::set($this->loadTemplate($template_string, NULL)->render($context));
+    return SafeString::create($this->loadTemplate($template_string, NULL)->render($context));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 1f6e1f904783..a59539492d12 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\GeneratedLink;
 use Drupal\Core\Link;
@@ -106,6 +107,13 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE)
       $variables['options']['attributes']['hreflang'] = $variables['options']['language']->getId();
     }
 
+    // Ensure that query values are strings.
+    array_walk($variables['options']['query'], function(&$value) {
+      if ($value instanceof SafeStringInterface) {
+        $value = (string) $value;
+      }
+    });
+
     // Set the "active" class if the 'set_active_class' option is not empty.
     if (!empty($variables['options']['set_active_class']) && !$url->isExternal()) {
       // Add a "data-drupal-link-query" attribute to let the
diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php
index 2dd2f402e19c..7e3c39a74aba 100644
--- a/core/modules/block/src/Tests/BlockViewBuilderTest.php
+++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
@@ -190,14 +190,14 @@ protected function verifyRenderCacheHandling() {
   public function testBlockViewBuilderAlter() {
     // Establish baseline.
     $build = $this->getBlockRenderArray();
-    $this->assertIdentical($this->renderer->renderRoot($build), 'Llamas &gt; unicorns!');
+    $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!');
 
     // Enable the block view alter hook that adds a suffix, for basic testing.
     \Drupal::state()->set('block_test_view_alter_suffix', TRUE);
     Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
     $build = $this->getBlockRenderArray();
     $this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '<br>Goodbye!', 'A block with content is altered.');
-    $this->assertIdentical($this->renderer->renderRoot($build), 'Llamas &gt; unicorns!<br>Goodbye!');
+    $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Llamas &gt; unicorns!<br>Goodbye!');
     \Drupal::state()->set('block_test_view_alter_suffix', FALSE);
 
     // Force a request via GET so we can test the render cache.
@@ -218,7 +218,7 @@ public function testBlockViewBuilderAlter() {
     $expected_keys = array_merge($default_keys, array($alter_add_key));
     $build = $this->getBlockRenderArray();
     $this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.');
-    $this->assertIdentical($this->renderer->renderRoot($build), '');
+    $this->assertIdentical((string) $this->renderer->renderRoot($build), '');
     $cache_entry = $this->container->get('cache.render')->get($cid);
     $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
     $expected_tags = array_merge($default_tags, ['rendered']);
@@ -233,7 +233,7 @@ public function testBlockViewBuilderAlter() {
     $build = $this->getBlockRenderArray();
     sort($build['#cache']['tags']);
     $this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.');
-    $this->assertIdentical($this->renderer->renderRoot($build), '');
+    $this->assertIdentical((string) $this->renderer->renderRoot($build), '');
     $cache_entry = $this->container->get('cache.render')->get($cid);
     $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
     $expected_tags = array_merge($default_tags, [$alter_add_tag, 'rendered']);
@@ -246,7 +246,7 @@ public function testBlockViewBuilderAlter() {
     \Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE);
     $build = $this->getBlockRenderArray();
     $this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before rendering.');
-    $this->assertIdentical($this->renderer->renderRoot($build), 'Hiya!<br>');
+    $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Hiya!<br>');
     $this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!<br>', 'A cached block without content is altered.');
 
     // Restore the previous request method.
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 5d4c26bc44ae..1f479f2bd602 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -143,7 +143,7 @@ function contact_mail($key, &$message, $params) {
       $message['subject'] .= t('[!form] !subject', $variables, $options);
       $message['body'][] = t("!sender-name (!sender-url) sent a message using the contact form at !form-url.", $variables, $options);
       $build = entity_view($contact_message, 'mail', $language->getId());
-      $message['body'][] = \Drupal::service('renderer')->renderPlain($build);
+      $message['body'][] = (string) \Drupal::service('renderer')->renderPlain($build);
       break;
 
     case 'page_autoreply':
@@ -162,7 +162,7 @@ function contact_mail($key, &$message, $params) {
       $message['body'][] = t("!sender-name (!sender-url) has sent you a message via your contact form at !site-name.", $variables, $options);
       $message['body'][] = t("If you don't want to receive such emails, you can change your settings at !recipient-edit-url.", $variables, $options);
       $build = entity_view($contact_message, 'mail', $language->getId());
-      $message['body'][] = \Drupal::service('renderer')->renderPlain($build);
+      $message['body'][] = (string) \Drupal::service('renderer')->renderPlain($build);
       break;
   }
 }
diff --git a/core/modules/contextual/src/ContextualController.php b/core/modules/contextual/src/ContextualController.php
index 975113dc9d1d..ca248d196453 100644
--- a/core/modules/contextual/src/ContextualController.php
+++ b/core/modules/contextual/src/ContextualController.php
@@ -44,7 +44,7 @@ public function render(Request $request) {
         '#type' => 'contextual_links',
         '#contextual_links' => _contextual_id_to_links($id),
       );
-      $rendered[$id] = $this->container->get('renderer')->renderRoot($element);
+      $rendered[$id] = (string) $this->container->get('renderer')->renderRoot($element);
     }
 
     return new JsonResponse($rendered);
diff --git a/core/modules/editor/src/EditorController.php b/core/modules/editor/src/EditorController.php
index 44feda30f836..192f3411e9bb 100644
--- a/core/modules/editor/src/EditorController.php
+++ b/core/modules/editor/src/EditorController.php
@@ -48,7 +48,7 @@ public function getUntransformedText(EntityInterface $entity, $field_name, $lang
     // Direct text editing is only supported for single-valued fields.
     $field = $entity->getTranslation($langcode)->$field_name;
     $editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
-    $response->addCommand(new GetUntransformedTextCommand($editable_text));
+    $response->addCommand(new GetUntransformedTextCommand((string) $editable_text));
 
     return $response;
   }
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index b05144361e86..67dbd6ceb8c7 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -288,7 +288,7 @@ function filter_fallback_format() {
  *   FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be
  *   skipped.
  *
- * @return string
+ * @return \Drupal\Component\Utility\SafeStringInterface
  *   The filtered text.
  *
  * @see filter_process_text()
diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php
index 4ad909b6a459..263b31ee48d6 100644
--- a/core/modules/filter/src/Tests/FilterAPITest.php
+++ b/core/modules/filter/src/Tests/FilterAPITest.php
@@ -60,7 +60,7 @@ function testCheckMarkupFilterOrder() {
     $text = "<p>Llamas are <not> awesome!</p>";
     $expected_filtered_text = "&lt;p&gt;Llamas are  awesome!&lt;/p&gt;";
 
-    $this->assertIdentical(check_markup($text, 'crazy'), $expected_filtered_text, 'Filters applied in correct order.');
+    $this->assertEqual(check_markup($text, 'crazy'), $expected_filtered_text, 'Filters applied in correct order.');
   }
 
   /**
@@ -73,14 +73,14 @@ function testCheckMarkupFilterSubset() {
 
     $actual_filtered_text = check_markup($text, 'filtered_html', '', array());
     $this->verbose("Actual:<pre>$actual_filtered_text</pre>Expected:<pre>$expected_filtered_text</pre>");
-    $this->assertIdentical(
+    $this->assertEqual(
       $actual_filtered_text,
       $expected_filtered_text,
       'Expected filter result.'
     );
     $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_MARKUP_LANGUAGE));
     $this->verbose("Actual:<pre>$actual_filtered_text_without_html_generators</pre>Expected:<pre>$expected_filter_text_without_html_generators</pre>");
-    $this->assertIdentical(
+    $this->assertEqual(
       $actual_filtered_text_without_html_generators,
       $expected_filter_text_without_html_generators,
       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters.'
@@ -91,7 +91,7 @@ function testCheckMarkupFilterSubset() {
     // most extensive test possible.
     $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE));
     $this->verbose("Actual:<pre>$actual_filtered_text_without_html_generators</pre>Expected:<pre>$expected_filter_text_without_html_generators</pre>");
-    $this->assertIdentical(
+    $this->assertEqual(
       $actual_filtered_text_without_html_generators,
       $expected_filter_text_without_html_generators,
       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.'
diff --git a/core/modules/quickedit/src/QuickEditController.php b/core/modules/quickedit/src/QuickEditController.php
index 383f61b8151b..067a73a7c322 100644
--- a/core/modules/quickedit/src/QuickEditController.php
+++ b/core/modules/quickedit/src/QuickEditController.php
@@ -216,7 +216,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
       $response->addCommand(new FieldFormSavedCommand($output, $other_view_modes));
     }
     else {
-      $output = $this->renderer->renderRoot($form);
+      $output = (string) $this->renderer->renderRoot($form);
       // When working with a hidden form, we don't want its CSS/JS to be loaded.
       if ($request->request->get('nocssjs') !== 'true') {
         $response->setAttachments($form['#attached']);
@@ -228,7 +228,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
         $status_messages = array(
           '#type' => 'status_messages'
         );
-        $response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages)));
+        $response->addCommand(new FieldFormValidationErrorsCommand((string) $this->renderer->renderRoot($status_messages)));
       }
     }
 
@@ -275,7 +275,7 @@ protected function renderField(EntityInterface $entity, $field_name, $langcode,
       $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
     }
 
-    return $this->renderer->renderRoot($output);
+    return (string) $this->renderer->renderRoot($output);
   }
 
   /**
diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
index 1eca0d2e8b15..a0f482adc63d 100644
--- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
+++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
@@ -108,7 +108,7 @@ public function testSerializerResponses() {
     // Mock the request content type by setting it on the display handler.
     $view->display_handler->setContentType('json');
     $output = $view->preview();
-    $this->assertIdentical($actual_json, drupal_render_root($output), 'The expected JSON preview output was found.');
+    $this->assertIdentical($actual_json, (string) drupal_render_root($output), 'The expected JSON preview output was found.');
 
     // Test a 403 callback.
     $this->drupalGet('test/serialize/denied');
diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php
index 99f7ca38970e..a425f4f954c6 100644
--- a/core/modules/simpletest/src/AssertContentTrait.php
+++ b/core/modules/simpletest/src/AssertContentTrait.php
@@ -398,7 +398,7 @@ protected function assertRaw($raw, $message = '', $group = 'Other') {
     if (!$message) {
       $message = SafeMarkup::format('Raw "@raw" found', array('@raw' => $raw));
     }
-    return $this->assert(strpos($this->getRawContent(), $raw) !== FALSE, $message, $group);
+    return $this->assert(strpos($this->getRawContent(), (string) $raw) !== FALSE, $message, $group);
   }
 
   /**
@@ -425,7 +425,7 @@ protected function assertNoRaw($raw, $message = '', $group = 'Other') {
     if (!$message) {
       $message = SafeMarkup::format('Raw "@raw" not found', array('@raw' => $raw));
     }
-    return $this->assert(strpos($this->getRawContent(), $raw) === FALSE, $message, $group);
+    return $this->assert(strpos($this->getRawContent(), (string) $raw) === FALSE, $message, $group);
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php b/core/modules/system/src/Tests/Common/RenderElementTypesTest.php
index d7c42cd10238..9c250fca7459 100644
--- a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php
+++ b/core/modules/system/src/Tests/Common/RenderElementTypesTest.php
@@ -43,7 +43,7 @@ protected function setUp() {
    *   Assertion message.
    */
   protected function assertElements(array $elements, $expected_html, $message) {
-    $actual_html = \Drupal::service('renderer')->renderRoot($elements);
+    $actual_html = (string) \Drupal::service('renderer')->renderRoot($elements);
 
     $out = '<table><tr>';
     $out .= '<td valign="top"><pre>' . SafeMarkup::checkPlain($expected_html) . '</pre></td>';
diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
index 11ec5898cca3..88f21c932e1c 100644
--- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
@@ -612,10 +612,10 @@ protected function doTestLanguageFallback($entity_type) {
     // Get an view builder.
     $controller = $this->entityManager->getViewBuilder($entity_type);
     $entity2_build = $controller->view($entity2);
-    $entity2_output = $renderer->renderRoot($entity2_build);
+    $entity2_output = (string) $renderer->renderRoot($entity2_build);
     $translation = $this->entityManager->getTranslationFromContext($entity2, $default_langcode);
     $translation_build = $controller->view($translation);
-    $translation_output = $renderer->renderRoot($translation_build);
+    $translation_output = (string) $renderer->renderRoot($translation_build);
     $this->assertIdentical($entity2_output, $translation_output, 'When the entity has no translation no fallback is applied.');
 
     // Checks that entity translations are rendered properly.
diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php
index 69c4f19fceb7..e56dcae7c9ee 100644
--- a/core/modules/views/src/Plugin/views/PluginBase.php
+++ b/core/modules/views/src/Plugin/views/PluginBase.php
@@ -390,7 +390,7 @@ function ($children, $elements) {
         ],
       );
 
-      return $this->getRenderer()->render($build);
+      return (string) $this->getRenderer()->render($build);
     }
     else {
       return $text;
diff --git a/core/modules/views/src/Plugin/views/display/Feed.php b/core/modules/views/src/Plugin/views/display/Feed.php
index 0798690bd995..e4da10e25b7d 100644
--- a/core/modules/views/src/Plugin/views/display/Feed.php
+++ b/core/modules/views/src/Plugin/views/display/Feed.php
@@ -67,7 +67,7 @@ public static function buildResponse($view_id, $display_id, array $args = []) {
     /** @var \Drupal\Core\Render\RendererInterface $renderer */
     $renderer = \Drupal::service('renderer');
 
-    $output = $renderer->renderRoot($build);
+    $output = (string) $renderer->renderRoot($build);
 
     if (empty($output)) {
       throw new NotFoundHttpException();
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 9bd01c4ef290..c83b9c4677f0 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -16,6 +16,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Renderer;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Url as CoreUrl;
 use Drupal\views\Plugin\views\HandlerBase;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -1137,7 +1138,7 @@ public function advancedRender(ResultRow $values) {
     else {
       $value = $this->render($values);
       if (is_array($value)) {
-        $value = $this->getRenderer()->render($value);
+        $value = (string) $this->getRenderer()->render($value);
       }
       $this->last_render = $value;
       $this->original_value = $value;
@@ -1150,7 +1151,7 @@ public function advancedRender(ResultRow $values) {
         foreach ($raw_items as $count => $item) {
           $value = $this->render_item($count, $item);
           if (is_array($value)) {
-            $value = $this->getRenderer()->render($value);
+            $value = (string) $this->getRenderer()->render($value);
           }
           $this->last_render = $value;
           $this->original_value = $this->last_render;
@@ -1168,7 +1169,7 @@ public function advancedRender(ResultRow $values) {
       }
 
       if (is_array($value)) {
-        $value = $this->getRenderer()->render($value);
+        $value = (string) $this->getRenderer()->render($value);
       }
       // This happens here so that renderAsLink can get the unaltered value of
       // this field as a token rather than the altered value.
@@ -1290,7 +1291,7 @@ public function renderText($alter) {
    * Render this field as user-defined altered text.
    */
   protected function renderAltered($alter, $tokens) {
-    return $this->viewsTokenReplace($alter['text'], $tokens);
+    return SafeString::create($this->viewsTokenReplace($alter['text'], $tokens));
   }
 
   /**
diff --git a/core/modules/views/src/Plugin/views/row/OpmlFields.php b/core/modules/views/src/Plugin/views/row/OpmlFields.php
index 2e936f58e4f5..4654bcad724f 100644
--- a/core/modules/views/src/Plugin/views/row/OpmlFields.php
+++ b/core/modules/views/src/Plugin/views/row/OpmlFields.php
@@ -217,7 +217,7 @@ public function getField($index, $field_id) {
     if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) {
       return '';
     }
-    return $this->view->style_plugin->getField($index, $field_id);
+    return (string) $this->view->style_plugin->getField($index, $field_id);
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php
index e1e977cf2b29..3bb472fb5bdf 100644
--- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php
+++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php
@@ -8,10 +8,10 @@
 namespace Drupal\views\Plugin\views\style;
 
 use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\SafeString;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
 use Drupal\views\Plugin\views\PluginBase;
 use Drupal\views\Plugin\views\wizard\WizardInterface;
@@ -706,8 +706,9 @@ protected function renderFields(array $result) {
             $placeholders = array_keys($post_render_tokens);
             $values = array_values($post_render_tokens);
             foreach ($this->rendered_fields[$index] as &$rendered_field) {
-              $rendered_field = str_replace($placeholders, $values, $rendered_field);
-              SafeMarkup::set($rendered_field);
+              // Placeholders and rendered fields have been processed by the
+              // render system and are therefore safe.
+              $rendered_field = SafeString::create(str_replace($placeholders, $values, $rendered_field));
             }
           }
         }
@@ -744,7 +745,7 @@ public function elementPreRenderRow(array $data) {
    * @param string $field
    *   The ID of the field.
    *
-   * @return string|null
+   * @return \Drupal\Core\Render\SafeString|null
    *   The output of the field, or NULL if it was empty.
    */
   public function getField($index, $field) {
diff --git a/core/modules/views/src/Tests/Handler/FieldCounterTest.php b/core/modules/views/src/Tests/Handler/FieldCounterTest.php
index 4d1eac1fd5cb..9fe24743cd3e 100644
--- a/core/modules/views/src/Tests/Handler/FieldCounterTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldCounterTest.php
@@ -51,11 +51,11 @@ function testSimple() {
     $view->preview();
 
     $counter = $view->style_plugin->getField(0, 'counter');
-    $this->assertEqual($counter, 1, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 1, '@counter' => $counter)));
+    $this->assertEqual($counter, '1', format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 1, '@counter' => $counter)));
     $counter = $view->style_plugin->getField(1, 'counter');
-    $this->assertEqual($counter, 2, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 2, '@counter' => $counter)));
+    $this->assertEqual($counter, '2', format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 2, '@counter' => $counter)));
     $counter = $view->style_plugin->getField(2, 'counter');
-    $this->assertEqual($counter, 3, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 3, '@counter' => $counter)));
+    $this->assertEqual($counter, '3', format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => 3, '@counter' => $counter)));
     $view->destroy();
     $view->storage->invalidateCaches();
 
@@ -80,13 +80,13 @@ function testSimple() {
 
     $counter = $view->style_plugin->getField(0, 'counter');
     $expected_number = 0 + $rand_start;
-    $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
+    $this->assertEqual($counter, (string) $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
     $counter = $view->style_plugin->getField(1, 'counter');
     $expected_number = 1 + $rand_start;
-    $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
+    $this->assertEqual($counter, (string) $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
     $counter = $view->style_plugin->getField(2, 'counter');
     $expected_number = 2 + $rand_start;
-    $this->assertEqual($counter, $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
+    $this->assertEqual($counter, (string) $expected_number, format_string('Make sure the expected number (@expected) patches with the rendered number (@counter)', array('@expected' => $expected_number, '@counter' => $counter)));
   }
 
   // @TODO: Write tests for pager.
diff --git a/core/modules/views/src/Tests/Handler/FieldFieldTest.php b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
index 8ad0bd14ba4e..599338581937 100644
--- a/core/modules/views/src/Tests/Handler/FieldFieldTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
@@ -251,18 +251,18 @@ public function testSimpleRender() {
     $executable = Views::getView('test_field_field_test');
     $executable->execute();
 
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'id'));
-    $this->assertEqual(3, $executable->getStyle()->getField(0, 'field_test'));
-    $this->assertEqual(2, $executable->getStyle()->getField(1, 'id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'id'));
+    $this->assertEqual('3', $executable->getStyle()->getField(0, 'field_test'));
+    $this->assertEqual('2', $executable->getStyle()->getField(1, 'id'));
     // @todo Switch this assertion to assertIdentical('', ...) when
     //   https://www.drupal.org/node/2488006 gets fixed.
-    $this->assertEqual(0, $executable->getStyle()->getField(1, 'field_test'));
-    $this->assertEqual(3, $executable->getStyle()->getField(2, 'id'));
-    $this->assertEqual(8, $executable->getStyle()->getField(2, 'field_test'));
-    $this->assertEqual(4, $executable->getStyle()->getField(3, 'id'));
-    $this->assertEqual(5, $executable->getStyle()->getField(3, 'field_test'));
-    $this->assertEqual(5, $executable->getStyle()->getField(4, 'id'));
-    $this->assertEqual(6, $executable->getStyle()->getField(4, 'field_test'));
+    $this->assertEqual('0', $executable->getStyle()->getField(1, 'field_test'));
+    $this->assertEqual('3', $executable->getStyle()->getField(2, 'id'));
+    $this->assertEqual('8', $executable->getStyle()->getField(2, 'field_test'));
+    $this->assertEqual('4', $executable->getStyle()->getField(3, 'id'));
+    $this->assertEqual('5', $executable->getStyle()->getField(3, 'field_test'));
+    $this->assertEqual('5', $executable->getStyle()->getField(4, 'id'));
+    $this->assertEqual('6', $executable->getStyle()->getField(4, 'field_test'));
   }
 
   /**
@@ -326,10 +326,10 @@ public function testFieldAliasRender() {
     $executable->execute();
 
     for ($i = 0; $i < 5; $i++) {
-      $this->assertEqual($i + 1, $executable->getStyle()->getField($i, 'id'));
+      $this->assertEqual((string) ($i + 1), $executable->getStyle()->getField($i, 'id'));
       $this->assertEqual('test ' . $i, $executable->getStyle()->getField($i, 'name'));
       $entity = EntityTest::load($i + 1);
-      $this->assertEqual('<a href="' . $entity->url() . '" hreflang="' . $entity->language()->getId() . '">test ' . $i . '</a>', $executable->getStyle()->getField($i, 'name_alias'));
+      $this->assertEqual('<a href="' . $entity->url() . '" hreflang="' . $entity->language()->getId() . '">test ' . $i . '</a>', (string) $executable->getStyle()->getField($i, 'name_alias'));
     }
   }
 
@@ -426,24 +426,24 @@ public function testRevisionRender() {
     $executable = Views::getView('test_field_field_revision_test');
     $executable->execute();
 
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'id'));
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id'));
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'field_test'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'revision_id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'field_test'));
     $this->assertEqual('base value', $executable->getStyle()->getField(0, 'name'));
 
-    $this->assertEqual(1, $executable->getStyle()->getField(1, 'id'));
-    $this->assertEqual(2, $executable->getStyle()->getField(1, 'revision_id'));
-    $this->assertEqual(2, $executable->getStyle()->getField(1, 'field_test'));
+    $this->assertEqual('1', $executable->getStyle()->getField(1, 'id'));
+    $this->assertEqual('2', $executable->getStyle()->getField(1, 'revision_id'));
+    $this->assertEqual('2', $executable->getStyle()->getField(1, 'field_test'));
     $this->assertEqual('revision value1', $executable->getStyle()->getField(1, 'name'));
 
-    $this->assertEqual(1, $executable->getStyle()->getField(2, 'id'));
-    $this->assertEqual(3, $executable->getStyle()->getField(2, 'revision_id'));
-    $this->assertEqual(3, $executable->getStyle()->getField(2, 'field_test'));
+    $this->assertEqual('1', $executable->getStyle()->getField(2, 'id'));
+    $this->assertEqual('3', $executable->getStyle()->getField(2, 'revision_id'));
+    $this->assertEqual('3', $executable->getStyle()->getField(2, 'field_test'));
     $this->assertEqual('revision value2', $executable->getStyle()->getField(2, 'name'));
 
-    $this->assertEqual(2, $executable->getStyle()->getField(3, 'id'));
-    $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id'));
-    $this->assertEqual(4, $executable->getStyle()->getField(3, 'field_test'));
+    $this->assertEqual('2', $executable->getStyle()->getField(3, 'id'));
+    $this->assertEqual('4', $executable->getStyle()->getField(3, 'revision_id'));
+    $this->assertEqual('4', $executable->getStyle()->getField(3, 'field_test'));
     $this->assertEqual('next entity value', $executable->getStyle()->getField(3, 'name'));
   }
 
@@ -484,29 +484,29 @@ public function testRevisionComplexRender() {
     $executable = Views::getView('test_field_field_revision_complex_test');
     $executable->execute();
 
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'id'));
-    $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(0, 'revision_id'));
     $this->assertEqual($this->testUsers[0]->getTimeZone(), $executable->getStyle()->getField(0, 'timezone'));
     $this->assertEqual('1, 3, 7', $executable->getStyle()->getField(0, 'field_test_multiple'));
     $this->assertEqual('1', $executable->getStyle()->getField(0, 'field_test_multiple_1'));
     $this->assertEqual('3, 7', $executable->getStyle()->getField(0, 'field_test_multiple_2'));
 
-    $this->assertEqual(1, $executable->getStyle()->getField(1, 'id'));
-    $this->assertEqual(2, $executable->getStyle()->getField(1, 'revision_id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(1, 'id'));
+    $this->assertEqual('2', $executable->getStyle()->getField(1, 'revision_id'));
     $this->assertEqual($this->testUsers[1]->getTimeZone(), $executable->getStyle()->getField(1, 'timezone'));
     $this->assertEqual('0, 3, 5', $executable->getStyle()->getField(1, 'field_test_multiple'));
     $this->assertEqual('0', $executable->getStyle()->getField(1, 'field_test_multiple_1'));
     $this->assertEqual('3, 5', $executable->getStyle()->getField(1, 'field_test_multiple_2'));
 
-    $this->assertEqual(1, $executable->getStyle()->getField(2, 'id'));
-    $this->assertEqual(3, $executable->getStyle()->getField(2, 'revision_id'));
+    $this->assertEqual('1', $executable->getStyle()->getField(2, 'id'));
+    $this->assertEqual('3', $executable->getStyle()->getField(2, 'revision_id'));
     $this->assertEqual($this->testUsers[2]->getTimeZone(), $executable->getStyle()->getField(2, 'timezone'));
     $this->assertEqual('9, 9, 9', $executable->getStyle()->getField(2, 'field_test_multiple'));
     $this->assertEqual('9', $executable->getStyle()->getField(2, 'field_test_multiple_1'));
     $this->assertEqual('9, 9', $executable->getStyle()->getField(2, 'field_test_multiple_2'));
 
-    $this->assertEqual(2, $executable->getStyle()->getField(3, 'id'));
-    $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id'));
+    $this->assertEqual('2', $executable->getStyle()->getField(3, 'id'));
+    $this->assertEqual('4', $executable->getStyle()->getField(3, 'revision_id'));
     $this->assertEqual($this->testUsers[3]->getTimeZone(), $executable->getStyle()->getField(3, 'timezone'));
     $this->assertEqual('2, 9, 9', $executable->getStyle()->getField(3, 'field_test_multiple'));
     $this->assertEqual('2', $executable->getStyle()->getField(3, 'field_test_multiple_1'));
@@ -531,9 +531,7 @@ public function testMissingBundleFieldRender() {
     $executable = Views::getView('test_field_field_test');
     $executable->execute();
 
-    // @todo Switch this assertion to assertIdentical('', ...) when
-    //   https://www.drupal.org/node/2488006 gets fixed.
-    $this->assertEqual(0, $executable->getStyle()->getField(1, 'field_test'));
+    $this->assertEqual('', $executable->getStyle()->getField(1, 'field_test'));
   }
 
 }
diff --git a/core/modules/views/src/Tests/QueryGroupByTest.php b/core/modules/views/src/Tests/QueryGroupByTest.php
index 768aaafbf0c7..4fb2062115ff 100644
--- a/core/modules/views/src/Tests/QueryGroupByTest.php
+++ b/core/modules/views/src/Tests/QueryGroupByTest.php
@@ -253,9 +253,9 @@ public function testGroupByFieldWithCardinality() {
     $this->executeView($view);
     $this->assertEqual(2, count($view->result));
 
-    $this->assertEqual(3, $view->getStyle()->getField(0, 'id'));
+    $this->assertEqual('3', $view->getStyle()->getField(0, 'id'));
     $this->assertEqual('1', $view->getStyle()->getField(0, 'field_test'));
-    $this->assertEqual(6, $view->getStyle()->getField(1, 'id'));
+    $this->assertEqual('6', $view->getStyle()->getField(1, 'id'));
     $this->assertEqual('2', $view->getStyle()->getField(1, 'field_test'));
 
     $entities[2]->field_test[0]->value = 3;
@@ -267,15 +267,15 @@ public function testGroupByFieldWithCardinality() {
     $this->executeView($view);
     $this->assertEqual(5, count($view->result));
 
-    $this->assertEqual(3, $view->getStyle()->getField(0, 'id'));
+    $this->assertEqual('3', $view->getStyle()->getField(0, 'id'));
     $this->assertEqual('1', $view->getStyle()->getField(0, 'field_test'));
-    $this->assertEqual(3, $view->getStyle()->getField(1, 'id'));
+    $this->assertEqual('3', $view->getStyle()->getField(1, 'id'));
     $this->assertEqual('2', $view->getStyle()->getField(1, 'field_test'));
-    $this->assertEqual(1, $view->getStyle()->getField(2, 'id'));
+    $this->assertEqual('1', $view->getStyle()->getField(2, 'id'));
     $this->assertEqual('3', $view->getStyle()->getField(2, 'field_test'));
-    $this->assertEqual(1, $view->getStyle()->getField(3, 'id'));
+    $this->assertEqual('1', $view->getStyle()->getField(3, 'id'));
     $this->assertEqual('4', $view->getStyle()->getField(3, 'field_test'));
-    $this->assertEqual(1, $view->getStyle()->getField(4, 'id'));
+    $this->assertEqual('1', $view->getStyle()->getField(4, 'id'));
     $this->assertEqual('5', $view->getStyle()->getField(4, 'field_test'));
 
     // Check that translated values are correctly retrieved and are not grouped
@@ -288,7 +288,7 @@ public function testGroupByFieldWithCardinality() {
     $this->executeView($view);
 
     $this->assertEqual(6, count($view->result));
-    $this->assertEqual(3, $view->getStyle()->getField(5, 'id'));
+    $this->assertEqual('3', $view->getStyle()->getField(5, 'id'));
     $this->assertEqual('6', $view->getStyle()->getField(5, 'field_test'));
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index 0f39b10dfd34..d659849f4713 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -179,7 +179,6 @@ public function providerTestContextBubblingEdgeCases() {
           'max-age' => Cache::PERMANENT,
         ],
         '#markup' => 'parent',
-        '#safe_cache_properties' => [],
       ],
     ];
     $data[] = [$test_element, [], $expected_cache_items];
@@ -203,7 +202,6 @@ public function providerTestContextBubblingEdgeCases() {
           'max-age' => Cache::PERMANENT,
         ],
         '#markup' => '',
-        '#safe_cache_properties' => [],
       ],
     ];
     $context_orders = [
@@ -245,7 +243,6 @@ public function providerTestContextBubblingEdgeCases() {
           'max-age' => 3600,
         ],
         '#markup' => 'parent',
-        '#safe_cache_properties' => [],
       ],
     ];
     $data[] = [$test_element, ['bar', 'baz', 'foo'], $expected_cache_items];
@@ -294,7 +291,6 @@ public function providerTestContextBubblingEdgeCases() {
           'max-age' => Cache::PERMANENT,
         ],
         '#markup' => 'parent',
-        '#safe_cache_properties' => [],
       ],
     ];
     $data[] = [$test_element, ['bar', 'foo'], $expected_cache_items];
@@ -374,7 +370,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => Cache::PERMANENT,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
 
     // Request 2: role B, the grandchild is accessible => bubbled cache
@@ -400,7 +395,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => 1800,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
 
     // Request 3: role A again, the grandchild is inaccessible again => bubbled
@@ -437,7 +431,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => Cache::PERMANENT,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
 
     // Request 4: role C, both the grandchild and the grandgrandchild are
@@ -465,7 +458,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => 300,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
 
     // Request 5: role A again, verifying the merging like we did for request 3.
@@ -485,7 +477,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => Cache::PERMANENT,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
 
     // Request 6: role B again, verifying the merging like we did for request 3.
@@ -505,7 +496,6 @@ public function testConditionalCacheContextBubblingSelfHealing() {
         'max-age' => 1800,
       ],
       '#markup' => 'parent',
-      '#safe_cache_properties' => [],
     ]);
   }
 
@@ -539,7 +529,7 @@ public function testBubblingWithPrerender($test_element) {
     // - … is not cached DOES get called.
     \Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE);
     \Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE);
-    $this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []], '#safe_cache_properties' => ['#markup']]);
+    $this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []]]);
 
     // Simulate the rendering of an entire response (i.e. a root call).
     $output = $this->renderer->renderRoot($test_element);
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index 8012a907f18b..9e3aaa76d65f 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -137,7 +137,6 @@ public function providerPlaceholders() {
           'tags' => [],
           'max-age' => Cache::PERMANENT,
         ],
-        '#safe_cache_properties' => [],
       ],
     ];
 
@@ -180,7 +179,6 @@ public function providerPlaceholders() {
           'tags' => [],
           'max-age' => Cache::PERMANENT,
         ],
-        '#safe_cache_properties' => [],
       ],
     ];
 
@@ -217,7 +215,7 @@ protected function assertPlaceholderRenderCache($cid_parts, array $expected_data
     if ($cid_parts !== FALSE) {
       // Verify render cached placeholder.
       $cached_element = $this->memoryCache->get(implode(':', $cid_parts))->data;
-      $this->assertSame($expected_data, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by the placeholder being replaced.');
+      $this->assertEquals($expected_data, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by the placeholder being replaced.');
     }
   }
   /**
@@ -239,8 +237,8 @@ public function testUncacheableParent($element, $args, $placeholder_cid_keys, ar
     // No #cache on parent element.
     $element['#prefix'] = '<p>#cache disabled</p>';
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', $output, 'Output is overridden.');
-    $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', $element['#markup'], '#markup is overridden.');
+    $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
+    $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = [
       'foo' => 'bar',
       'dynamic_animal' => $args[0],
@@ -285,9 +283,9 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys,
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', $output, 'Output is overridden.');
+    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
-    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', $element['#markup'], '#markup is overridden.');
+    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = [
       'foo' => 'bar',
       'dynamic_animal' => $args[0],
@@ -314,7 +312,6 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys,
         'tags' => [],
         'max-age' => Cache::PERMANENT,
       ],
-      '#safe_cache_properties' => [],
     ];
     $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
     $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
@@ -324,9 +321,9 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys,
     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
     $element['#prefix'] = '<p>#cache enabled, GET</p>';
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', $output, 'Output is overridden.');
+    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
-    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', $element['#markup'], '#markup is overridden.');
+    $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = [
       'foo' => 'bar',
       'dynamic_animal' => $args[0],
@@ -354,9 +351,9 @@ public function testCacheableParentWithPostRequest($test_element, $args) {
     $element['#cache'] = ['keys' => ['placeholder_test_POST']];
     $element['#prefix'] = '<p>#cache enabled, POST</p>';
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', $output, 'Output is overridden.');
+    $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
-    $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', $element['#markup'], '#markup is overridden.');
+    $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
     $expected_js_settings = [
       'foo' => 'bar',
       'dynamic_animal' => $args[0],
@@ -386,7 +383,7 @@ public function testRecursivePlaceholder() {
 
     $output = $this->renderer->renderRoot($element);
     $this->assertEquals('<p>This is a rendered placeholder!</p>', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.');
-    $this->assertSame($element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
+    $this->assertSame((string) $element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
     $expected_js_settings = [
       'dynamic_animal' => $args[0],
     ];
@@ -549,9 +546,9 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
 </details></div>
 </details>
 HTML;
-    $this->assertSame($expected_output, $output, 'Output is not overridden.');
+    $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
-    $this->assertSame($expected_output, $output, '#markup is not overridden.');
+    $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
     $expected_js_settings = [
       'foo' => 'bar',
       'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE],
@@ -582,7 +579,6 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
         'tags' => [],
         'max-age' => Cache::PERMANENT,
       ],
-      '#safe_cache_properties' => [],
     ];
 
     $dom = Html::load($cached_element['#markup']);
@@ -599,7 +595,7 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
     // GET request: #cache enabled, cache hit.
     $element = $test_element;
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame($expected_output, $output, 'Output is not overridden.');
+    $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
 
@@ -608,8 +604,8 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
     unset($test_element['#cache']);
     $element = $test_element;
     $output = $this->renderer->renderRoot($element);
-    $this->assertSame($expected_output, $output, 'Output is not overridden.');
-    $this->assertSame($expected_output, $output, '#markup is not overridden.');
+    $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
+    $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #lazy_builder callback exist.');
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 8ffe61fbf7fe..c6845f560ddb 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Template\Attribute;
 
 /**
@@ -46,7 +47,7 @@ public function testRenderBasic($build, $expected, callable $setup_code = NULL)
       $setup_code();
     }
 
-    $this->assertSame($expected, $this->renderer->renderRoot($build));
+    $this->assertSame($expected, (string) $this->renderer->renderRoot($build));
   }
 
   /**
@@ -508,10 +509,10 @@ protected function assertAccess($build, $access) {
     $sensitive_content = $this->randomContextValue();
     $build['#markup'] = $sensitive_content;
     if (($access instanceof AccessResultInterface && $access->isAllowed()) || $access === TRUE) {
-      $this->assertSame($sensitive_content, $this->renderer->renderRoot($build));
+      $this->assertSame($sensitive_content, (string) $this->renderer->renderRoot($build));
     }
     else {
-      $this->assertSame('', $this->renderer->renderRoot($build));
+      $this->assertSame('', (string) $this->renderer->renderRoot($build));
     }
   }
 
@@ -691,8 +692,8 @@ public function testRenderCacheProperties(array $expected_results) {
       ],
       // Collect expected property names.
       '#cache_properties' => array_keys(array_filter($expected_results)),
-      'child1' => ['#markup' => '1'],
-      'child2' => ['#markup' => '2'],
+      'child1' => ['#markup' => SafeString::create('1')],
+      'child2' => ['#markup' => SafeString::create('2')],
       // Mark the value as safe.
       '#custom_property' => SafeMarkup::checkPlain('custom_value'),
       '#custom_property_array' => ['custom value'],
@@ -712,7 +713,7 @@ public function testRenderCacheProperties(array $expected_results) {
       $this->assertEquals($cached, (bool) $expected);
       // Check that only the #markup key is preserved for children.
       if ($cached) {
-        $this->assertSame($data[$property], $original[$property]);
+        $this->assertEquals($data[$property], $original[$property]);
       }
     }
     // #custom_property_array can not be a safe_cache_property.
-- 
GitLab