diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index da3e02a..455cd7a 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -546,16 +546,15 @@ public static function linkGenerator() {
    *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
-   *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   * @return \Drupal\Core\GeneratedLink
+   *   A GeneratedLink object containing a link to the given route and
+   *   parameters and bubbleable metadata.
    *
    * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
    * @see \Drupal\Core\Url
    */
-  public static function l($text, Url $url, $collect_bubbleable_metadata = FALSE) {
-    return static::getContainer()->get('link_generator')->generate($text, $url, $collect_bubbleable_metadata);
+  public static function l($text, Url $url) {
+    return static::getContainer()->get('link_generator')->generate($text, $url);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/GeneratedLink.php b/core/lib/Drupal/Core/GeneratedLink.php
index eb35bc6..bfd9769 100644
--- a/core/lib/Drupal/Core/GeneratedLink.php
+++ b/core/lib/Drupal/Core/GeneratedLink.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core;
 
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 
 /**
@@ -15,7 +16,7 @@
  * Note: not to be confused with \Drupal\Core\Link, which is for passing around
  *   ungenerated links (typically link text + route name + route parameters).
  */
-class GeneratedLink extends BubbleableMetadata {
+class GeneratedLink extends BubbleableMetadata implements SafeStringInterface {
 
   /**
    * The HTML string value containing a link.
@@ -46,4 +47,18 @@ public function setGeneratedLink($generated_link) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    return (string) $this->generatedLink;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Link.php b/core/lib/Drupal/Core/Link.php
index 0aa9d07..8db6591 100644
--- a/core/lib/Drupal/Core/Link.php
+++ b/core/lib/Drupal/Core/Link.php
@@ -144,16 +144,14 @@ public function setUrl(Url $url) {
    *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
    *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
+   * @return \Drupal\Core\GeneratedLink
    *   The link HTML markup.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
    *
    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
    *   self::toRenderable() instead.
    */
-  public function toString($collect_bubbleable_metadata = FALSE) {
-    return $this->getLinkGenerator()->generateFromLink($this, $collect_bubbleable_metadata);
+  public function toString() {
+    return $this->getLinkGenerator()->generateFromLink($this);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/menu.api.php b/core/lib/Drupal/Core/Menu/menu.api.php
index 0ae528f..b6ad987 100644
--- a/core/lib/Drupal/Core/Menu/menu.api.php
+++ b/core/lib/Drupal/Core/Menu/menu.api.php
@@ -440,7 +440,11 @@ function hook_system_breadcrumb_alter(\Drupal\Core\Breadcrumb\Breadcrumb &$bread
  *   exposed as the 'link_generator' service or a link generated by _l(). If the
  *   link is a "route link", 'route_name' will be set, otherwise 'path' will be
  *   set. The following keys can be altered:
- *   - text: The link text for the anchor tag as a translated string.
+ *   - text: The link text for the anchor tag. If the hook implementation
+ *     changes this text is needs to preserve the safeness of the original text.
+ *     Using t() or \Drupal\Component\Utility\SafeMarkup::format() with
+ *     @placeholder is recommended as this will escape the original text if
+ *     necessary. If the resulting text is not marked safe it will be escaped.
  *   - url_is_active: Whether or not the link points to the currently active
  *     URL.
  *   - url: The \Drupal\Core\Url object.
@@ -468,7 +472,7 @@ function hook_system_breadcrumb_alter(\Drupal\Core\Breadcrumb\Breadcrumb &$bread
 function hook_link_alter(&$variables) {
   // Add a warning to the end of route links to the admin section.
   if (isset($variables['route_name']) && strpos($variables['route_name'], 'admin') !== FALSE) {
-    $variables['text'] .= ' (Warning!)';
+    $variables['text'] = t('@text (Warning!)', ['@text' => $variables['text']]);
   }
 }
 
diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php
index bec2341..93ab693 100644
--- a/core/lib/Drupal/Core/Render/Element/Link.php
+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -82,7 +82,7 @@ public static function preRenderLink($element) {
       $options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']);
       /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */
       $link_generator = \Drupal::service('link_generator');
-      $generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
+      $generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options));
       $element['#markup'] = $generated_link->getGeneratedLink();
       $generated_link->merge(BubbleableMetadata::createFromRenderArray($element))
         ->applyTo($element);
diff --git a/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php b/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
index 35170d1..a199288 100644
--- a/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
+++ b/core/lib/Drupal/Core/Routing/LinkGeneratorTrait.php
@@ -35,7 +35,7 @@
    * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate() for details
    *   on the arguments, usage, and possible exceptions.
    *
-   * @return string
+   * @return \Drupal\Core\GeneratedLink
    *   An HTML string containing a link to the given route and parameters.
    */
   protected function l($text, Url $url) {
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index a595394..a3d991a 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -8,10 +8,12 @@
 namespace Drupal\Core\Utility;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\GeneratedLink;
+use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Link;
 use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Render\RendererInterface;
@@ -64,8 +66,8 @@ public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerI
   /**
    * {@inheritdoc}
    */
-  public function generateFromLink(Link $link, $collect_bubbleable_metadata = FALSE) {
-    return $this->generate($link->getText(), $link->getUrl(), $collect_bubbleable_metadata);
+  public function generateFromLink(Link $link) {
+    return $this->generate($link->getText(), $link->getUrl());
   }
 
   /**
@@ -80,14 +82,18 @@ public function generateFromLink(Link $link, $collect_bubbleable_metadata = FALS
    *
    * @see system_page_attachments()
    */
-  public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE) {
+  public function generate($text, Url $url) {
     // Performance: avoid Url::toString() needing to retrieve the URL generator
     // service from the container.
     $url->setUrlGenerator($this->urlGenerator);
 
+    if (is_array($text)) {
+      $text = $this->renderer->render($text);
+    }
+
     // Start building a structured representation of our link to be altered later.
     $variables = array(
-      'text' => is_array($text) ? $this->renderer->render($text) : $text,
+      'text' => $text,
       'url' => $url,
       'options' => $url->getOptions(),
     );
@@ -149,21 +155,26 @@ public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE)
     unset($variables['options']['attributes']);
     $url->setOptions($variables['options']);
 
-    if (!$collect_bubbleable_metadata) {
-      $url_string = $url->toString($collect_bubbleable_metadata);
+    // External urls can not have cacheable metadata.
+    if ($url->isExternal()) {
+      $generated_link = new GeneratedLink();
+      $attributes['href'] = $url->toString(FALSE);
     }
     else {
-      $generated_url = $url->toString($collect_bubbleable_metadata);
-      $url_string = $generated_url->getGeneratedUrl();
+      $generated_url = $url->toString(TRUE);
       $generated_link = GeneratedLink::createFromObject($generated_url);
+      // The result of the URL generator is a plain-text URL to use as the href
+      // attribute, and it is escaped by \Drupal\Core\Template\Attribute.
+      $attributes['href'] = $generated_url->getGeneratedUrl();
     }
-    // The result of the URL generator is a plain-text URL to use as the href
-    // attribute, and it is escaped by \Drupal\Core\Template\Attribute.
-    $attributes['href'] = $url_string;
 
-    $result = SafeMarkup::format('<a@attributes>@text</a>', array('@attributes' => new Attribute($attributes), '@text' => $variables['text']));
-
-    return $collect_bubbleable_metadata ? $generated_link->setGeneratedLink($result) : $result;
+    if (!SafeMarkup::isSafe($variables['text'])) {
+      $variables['text'] = Html::escape($variables['text']);
+    }
+    $attributes = new Attribute($attributes);
+    // This is safe because Attribute does escaping and $variables['text'] is
+    // either rendered or escaped.
+    return $generated_link->setGeneratedLink('<a' . $attributes . '>' . $variables['text'] . '</a>');
   }
 
 }
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 733c449..383bf83 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -60,14 +60,10 @@
    *     class will be applied to the link. It is important to use this
    *     sparingly since it is usually unnecessary and requires extra
    *     processing.
-   * @param bool $collect_bubbleable_metadata
-   *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
-   *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
-   *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   * @return \Drupal\Core\GeneratedLink
+   *   A GeneratedLink object containing a link to the given route and
+   *   parameters and bubbleable metadata.
    *
    * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
    *   Thrown when the named route doesn't exist.
@@ -77,22 +73,18 @@
    *   Thrown when a parameter value for a placeholder is not correct because it
    *   does not match the requirement.
    */
-  public function generate($text, Url $url, $collect_bubbleable_metadata = FALSE);
+  public function generate($text, Url $url);
 
   /**
    * Renders a link from a link object.
    *
    * @param \Drupal\Core\Link $link
    *   A link object to convert to a string.
-   * @param bool $collect_bubbleable_metadata
-   *   (optional) Defaults to FALSE. When TRUE, both the generated link and its
-   *   associated bubbleable metadata are returned.
    *
-   * @return string|\Drupal\Core\GeneratedLink
-   *   An HTML string containing a link to the given route and parameters.
-   *   When $collect_bubbleable_metadata is TRUE, a GeneratedLink object is
-   *   returned, containing the generated link plus bubbleable metadata.
+   * @return \Drupal\Core\GeneratedLink
+   *   A GeneratedLink object containing a link to the given route and
+   *   parameters and bubbleable metadata.
    */
-  public function generateFromLink(Link $link, $collect_bubbleable_metadata = FALSE);
+  public function generateFromLink(Link $link);
 
 }
diff --git a/core/modules/system/src/Tests/Utility/LinkGenerationTest.php b/core/modules/system/src/Tests/Utility/LinkGenerationTest.php
new file mode 100644
index 0000000..62c4d23
--- /dev/null
+++ b/core/modules/system/src/Tests/Utility/LinkGenerationTest.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Utility\LinkGenerationTest.
+ */
+
+namespace Drupal\system\Tests\Utility;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Url;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests link generation with hooks.
+ *
+ * @group Utility
+ */
+class LinkGenerationTest extends KernelTestBase {
+
+  public static $modules = ['link_generation_test'];
+
+  /**
+   * Tests how hook_link_alter() can affect escaping of the link text.
+   */
+  function testHookLinkAlter() {
+    $url = Url::fromUri('http://example.com');
+    $renderer = \Drupal::service('renderer');
+
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is not escaped.
+    $this->assertRaw('<em>link with markup</em>');
+
+    // Test just adding text to an already safe string.
+    \Drupal::state()->set('link_generation_test_link_alter', TRUE);
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertEscaped('<em>link with markup</em> <strong>Test!</strong>');
+
+    // Test passing a safe string to t().
+    \Drupal::state()->set('link_generation_test_link_alter_safe', TRUE);
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l(['#markup' => '<em>link with markup</em>'], $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertRaw('<em>link with markup</em> <strong>Test!</strong>');
+
+    // Test passing an unsafe string to t().
+    $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
+      return \Drupal::l('<em>link with markup</em>', $url);
+    });
+    $this->setRawContent($link);
+    $this->assertTrue(SafeMarkup::isSafe($link), 'The output of link generation is marked safe as it is a link.');
+    // Ensure the content of the link is escaped.
+    $this->assertEscaped('<em>link with markup</em>');
+    $this->assertRaw('<strong>Test!</strong>');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml b/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml
new file mode 100644
index 0000000..2f200a4
--- /dev/null
+++ b/core/modules/system/tests/modules/link_generation_test/link_generation_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Link generation test support'
+type: module
+description: 'Test hooks fired in link generation.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/link_generation_test/link_generation_test.module b/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
new file mode 100644
index 0000000..622a403
--- /dev/null
+++ b/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * Helper module for the link generation tests.
+ */
+
+/**
+ * Implements hook_link_alter().
+ */
+function link_generation_test_link_alter(&$variables) {
+  if (\Drupal::state()->get('link_generation_test_link_alter', FALSE)) {
+    // Add a text to the end of links.
+    if (\Drupal::state()->get('link_generation_test_link_alter_safe', FALSE)) {
+      $variables['text'] = t('@text <strong>Test!</strong>', ['@text' => $variables['text']]);
+    }
+    else {
+      $variables['text'] .= ' <strong>Test!</strong>';
+    }
+  }
+}
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 29abf88..f168e6b 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -1307,8 +1307,10 @@ public function renderText($alter) {
     }
 
     // Preserve whether or not the string is safe. Since $suffix comes from
-    // \Drupal::l(), it is safe to append.
-    if ($value_is_safe) {
+    // \Drupal::l(), it is safe to append. Use SafeMarkup::isSafe() here because
+    // $value_is_safe is safe has been used above and renderAsLink() can return
+    // both safe and unsafe values.
+    if (SafeMarkup::isSafe($value)) {
       return ViewsRenderPipelineSafeString::create($value . $suffix);
     }
     else {
@@ -1533,19 +1535,20 @@ protected function renderAsLink($alter, $text, $tokens) {
 
     // Build the link based on our altered Url object, adding on the optional
     // prefix and suffix
-    $value = '';
+    $render = [
+      '#type' => 'link',
+      '#title' => $text,
+      '#url' => $final_url,
+    ];
 
     if (!empty($alter['prefix'])) {
-      $value .= Xss::filterAdmin($this->viewsTokenReplace($alter['prefix'], $tokens));
+      $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
     }
-
-    $value .= $this->linkGenerator()->generate($text, $final_url);
-
     if (!empty($alter['suffix'])) {
-      $value .= Xss::filterAdmin($this->viewsTokenReplace($alter['suffix'], $tokens));
+      $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
     }
+    return $this->getRenderer()->render($render);
 
-    return $value;
   }
 
   /**
diff --git a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php
index 65471b7..9dfbb06 100644
--- a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php
+++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php
@@ -7,7 +7,10 @@
 
 namespace Drupal\Tests\views\Unit\Plugin\field;
 
+use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Language\Language;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\LinkGenerator;
 use Drupal\Core\Utility\LinkGeneratorInterface;
@@ -174,6 +177,22 @@ protected function setUpUrlIntegrationServices() {
     \Drupal::getContainer()->set('unrouted_url_assembler', $this->unroutedUrlAssembler);
 
     $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'), $this->renderer);
+    $this->renderer
+      ->method('render')
+      ->willReturnCallback(
+        // Pretend to do a render.
+        function (&$elements, $is_root_call = FALSE) {
+          // Mock the ability to theme links
+          $link = $this->linkGenerator->generate($elements['#title'], $elements['#url']);
+          if (isset($elements['#prefix'])) {
+            $link = $elements['#prefix'] . $link;
+          }
+          if (isset($elements['#suffix'])) {
+            $link = $link . $elements['#suffix'];
+          }
+          return SafeString::create($link);
+        }
+      );
   }
 
   /**
@@ -229,7 +248,7 @@ public function testRenderAsLinkWithPathAndOptions($path, $alter, $link_html, $f
     $row = new ResultRow(['key' => 'value']);
 
     $result = $field->advancedRender($row);
-    $this->assertEquals($final_html, $result);
+    $this->assertEquals($final_html, (string) $result);
   }
 
   /**
@@ -308,8 +327,8 @@ public function testRenderAsLinkWithUrlAndOptions(Url $url, $alter, Url $expecte
 
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
-      ->with($expected_url->getRouteName(), $expected_url->getRouteParameters(), $expected_url_options)
-      ->willReturn($url_path);
+      ->with($expected_url->getRouteName(), $expected_url->getRouteParameters(), $expected_url_options, TRUE)
+      ->willReturn((new GeneratedUrl())->setGeneratedUrl($url_path));
 
     $result = $field->advancedRender($row);
     $this->assertEquals($final_html, $result);
diff --git a/core/modules/views_ui/src/Controller/ViewsUIController.php b/core/modules/views_ui/src/Controller/ViewsUIController.php
index 2837508..be71932 100644
--- a/core/modules/views_ui/src/Controller/ViewsUIController.php
+++ b/core/modules/views_ui/src/Controller/ViewsUIController.php
@@ -121,13 +121,15 @@ public function reportFields() {
   public function reportPlugins() {
     $rows = Views::pluginList();
     foreach ($rows as &$row) {
+      $views = [];
       // Link each view name to the view itself.
       foreach ($row['views'] as $row_name => $view) {
-        $row['views'][$row_name] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
+        $views[] = $this->l($view, new Url('entity.view.edit_form', array('view' => $view)));
       }
+      unset($row['views']);
       $row['views']['data'] = [
         '#theme' => 'item_list',
-        '#items' => $row['views'],
+        '#items' => $views,
         '#context' => ['list_style' => 'comma-list'],
       ];
     }
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index 6659945..eb296cf 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Utility {
 
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Link;
@@ -111,8 +112,10 @@ public function testGenerateHrefs($route_name, array $parameters, $absolute, $ex
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with($route_name, $parameters, array('absolute' => $absolute) + $this->defaultOptions)
-      ->will($this->returnValue($expected_url));
-
+      ->willReturnCallback(function() use ($expected_url) {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl($expected_url);
+      });
     $this->moduleHandler->expects($this->once())
       ->method('alter');
 
@@ -133,7 +136,10 @@ public function testGenerate() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_1', array(), array('fragment' => 'the-fragment') + $this->defaultOptions)
-      ->will($this->returnValue('/test-route-1#the-fragment'));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-1#the-fragment');
+      });
 
     $this->moduleHandler->expects($this->once())
       ->method('alter')
@@ -197,7 +203,7 @@ public function testGenerateUrlWithQuotes() {
     $this->urlAssembler->expects($this->once())
       ->method('assemble')
       ->with('base:example', array('query' => array('foo' => '"bar"', 'zoo' => 'baz')) + $this->defaultOptions)
-      ->will($this->returnValue('/example?foo=%22bar%22&zoo=baz'));
+      ->willReturn((new GeneratedUrl())->setGeneratedUrl('/example?foo=%22bar%22&zoo=baz'));
 
     $path_validator = $this->getMock('Drupal\Core\Path\PathValidatorInterface');
     $container_builder = new ContainerBuilder();
@@ -228,9 +234,7 @@ public function testGenerateAttributes() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_1', array(), $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-1'
-      ));
+      ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1'));
 
     // Test that HTML attributes are added to the anchor.
     $url = new Url('test_route_1', array(), array(
@@ -255,9 +259,10 @@ public function testGenerateQuery() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_1', array(), array('query' => array('test' => 'value')) + $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-1?test=value'
-      ));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-1?test=value');
+      });
 
     $url = new Url('test_route_1', array(), array(
       'query' => array('test' => 'value'),
@@ -280,9 +285,10 @@ public function testGenerateParametersAsQuery() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_1', array('test' => 'value'), $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-1?test=value'
-      ));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-1?test=value');
+      });
 
     $url = new Url('test_route_1', array('test' => 'value'), array());
     $url->setUrlGenerator($this->urlGenerator);
@@ -303,10 +309,10 @@ public function testGenerateOptions() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_1', array(), array('key' => 'value') + $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-1?test=value'
-      ));
-
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-1?test=value');
+      });
     $url = new Url('test_route_1', array(), array(
       'key' => 'value',
     ));
@@ -328,9 +334,10 @@ public function testGenerateXss() {
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->with('test_route_4', array(), $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-4'
-      ));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-4');
+      });
 
     // Test that HTML link text is escaped by default.
     $url = new Url('test_route_4');
@@ -348,15 +355,17 @@ public function testGenerateWithHtml() {
     $this->urlGenerator->expects($this->at(0))
       ->method('generateFromRoute')
       ->with('test_route_5', array(), $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-5'
-      ));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-5');
+      });
     $this->urlGenerator->expects($this->at(1))
       ->method('generateFromRoute')
       ->with('test_route_5', array(), $this->defaultOptions)
-      ->will($this->returnValue(
-        '/test-route-5'
-      ));
+      ->willReturnCallback(function() {
+        $url = new GeneratedUrl();
+        return $url->setGeneratedUrl('/test-route-5');
+      });
 
     // Test that HTML tags are stripped from the 'title' attribute.
     $url = new Url('test_route_5', array(), array(
@@ -394,11 +403,18 @@ public function testGenerateWithHtml() {
   public function testGenerateActive() {
     $this->urlGenerator->expects($this->exactly(5))
       ->method('generateFromRoute')
-      ->will($this->returnValueMap(array(
-        array('test_route_1', array(), FALSE, '/test-route-1'),
-        array('test_route_3', array(), FALSE, '/test-route-3'),
-        array('test_route_4', array('object' => '1'), FALSE, '/test-route-4/1'),
-      )));
+      ->willReturnCallback(function($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE) {
+        switch ($name) {
+          case 'test_route_1':
+            return (new GeneratedUrl())->setGeneratedUrl('/test-route-1');
+          case 'test_route_3':
+            return (new GeneratedUrl())->setGeneratedUrl('/test-route-3');
+          case 'test_route_4':
+            if ($parameters['object'] == '1') {
+              return (new GeneratedUrl())->setGeneratedUrl('/test-route-4/1');
+            }
+        }
+      });
 
     $this->urlGenerator->expects($this->exactly(4))
       ->method('getPathFromRoute')
@@ -479,7 +495,6 @@ public function testGenerateBubbleableMetadata() {
     $this->urlGenerator->expects($this->any())
       ->method('generateFromRoute')
       ->will($this->returnValueMap([
-        ['test_route_1', [], $options, FALSE, '/test-route-1'],
         ['test_route_1', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-1')],
       ]));
 
@@ -488,16 +503,16 @@ public function testGenerateBubbleableMetadata() {
     $expected_link_markup = '<a href="/test-route-1">Test</a>';
 
     // Test ::generate().
-    $this->assertSame($expected_link_markup, $this->linkGenerator->generate('Test', $url));
-    $generated_link = $this->linkGenerator->generate('Test', $url, TRUE);
-    $this->assertSame($expected_link_markup, $generated_link->getGeneratedLink());
+    $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url));
+    $generated_link = $this->linkGenerator->generate('Test', $url);
+    $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
 
     // Test ::generateFromLink().
     $link = new Link('Test', $url);
-    $this->assertSame($expected_link_markup, $this->linkGenerator->generateFromLink($link));
-    $generated_link = $this->linkGenerator->generateFromLink($link, TRUE);
-    $this->assertSame($expected_link_markup, $generated_link->getGeneratedLink());
+    $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generateFromLink($link));
+    $generated_link = $this->linkGenerator->generateFromLink($link);
+    $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
   }
 
@@ -508,12 +523,12 @@ public function testGenerateBubbleableMetadata() {
    *   An associative array of link properties, with the following keys:
    *   - attributes: optional array of HTML attributes that should be present.
    *   - content: optional link content.
-   * @param string $html
+   * @param \Drupal\Component\Utility\SafeStringInterface $html
    *   The HTML to check.
    * @param int $count
    *   How many times the link should be present in the HTML. Defaults to 1.
    */
-  public static function assertLink(array $properties, $html, $count = 1) {
+  public static function assertLink(array $properties, SafeStringInterface $html, $count = 1) {
     // Provide default values.
     $properties += array('attributes' => array());
 
