Repository: intellij-rust/intellij-rust
Issue: Wrong web links to Clippy lint online documentation

## Environment

* **IntelliJ Rust plugin version:** 0.4.147.3871-211
* **Rust toolchain version:** 1.52.0 (88f19c6da 2021-05-03) x86_64-unknown-linux-gnu
* **IDE name and version:** CLion 2021.1 (CL-211.6693.114)
* **Operating system:** Linux 5.4.0-72-generic
* **Macro expansion engine:** new
* **Name resolution engine:** new

## Problem description

When a Clippy lint files, the lint description in the IDE provides an active link to the web page with the supposed link description. However, the link is wrong.

## Steps to reproduce

Consider the following code:

```rust
#![deny(clippy::unwrap_used)]

fn foo(x: Option<usize>) -> usize {
    x.unwrap()
//  ^^^^^^^^^^ error here
}
```

Hovering over the underlined error in the IDE brings up the following error message:

```
used `unwrap()` on `an Option` value clippy::unwrap_used 
Note: the lint level is defined here
Help: if you don't want to handle the `None` case gracefully, consider using `expect()` to provide a better panic message 
Help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used
```

Here `"clippy::unwrap_used"` is an active link which points to 

    https://doc.rust-lang.org/error-index.html#clippy::unwrap_used

This link is also obviously wrong: that's a compiler error index, not a Clippy lint index. The correct link should have the form 

    https://rust-lang.github.io/rust-clippy/master/#unwrap_used

which is, by the way, listed in the error message but isn't clickable.


Changed files:
src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt, src/main/kotlin/org/rust/ide/annotator/RsExternalLinterUtils.kt

diff --git a/src/main/kotlin/org/rust/ide/annotator/RsExternalLinterUtils.kt b/src/main/kotlin/org/rust/ide/annotator/RsExternalLinterUtils.kt
index f4cc6c20a..4cb25935b 100644
--- a/src/main/kotlin/org/rust/ide/annotator/RsExternalLinterUtils.kt
+++ b/src/main/kotlin/org/rust/ide/annotator/RsExternalLinterUtils.kt
@@ -29,6 +29,7 @@ import com.intellij.psi.impl.AnyPsiChangeListener
 import com.intellij.psi.impl.PsiManagerImpl
 import com.intellij.psi.util.PsiModificationTracker
 import com.intellij.util.PathUtil
+import com.intellij.util.io.URLUtil
 import com.intellij.util.messages.MessageBus
 import org.apache.commons.lang.StringEscapeUtils
 import org.rust.cargo.project.workspace.PackageOrigin
@@ -257,6 +258,7 @@ private data class RsExternalLinterFilteredMessage(
                     .forEach { add(it) }
 
                 joinToString("<br>") { formatMessage(it) }
+                    .replace(URL_REGEX) { url -> "<a href='${url.value}'>${url.value}</a>" }
             }
 
             return RsExternalLinterFilteredMessage(
@@ -273,8 +275,13 @@ private data class RsExternalLinterFilteredMessage(
 fun RustcSpan.isValid(): Boolean =
     line_end > line_start || (line_end == line_start && column_end >= column_start)
 
-private fun ErrorCode?.formatAsLink(): String? =
-    if (this?.code.isNullOrBlank()) null else "<a href=\\"${RsConstants.ERROR_INDEX_URL}#${this?.code}\\">${this?.code}</a>"
+private val ERROR_REGEX: Regex = """E\\d{4}""".toRegex()
+private val URL_REGEX: Regex = URLUtil.URL_PATTERN.toRegex()
+
+private fun ErrorCode?.formatAsLink(): String? {
+    if (this?.code?.matches(ERROR_REGEX) != true) return null
+    return "<a href=\\"${RsConstants.ERROR_INDEX_URL}#$code\\">$code</a>"
+}
 
 private fun RustcMessage.collectQuickFixes(file: PsiFile, document: Document): List<ApplySuggestionFix> {
     val quickFixes = mutableListOf<ApplySuggestionFix>()
diff --git a/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt b/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
index 4ecb3a9d6..3657ec069 100644
--- a/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
+++ b/src/test/kotlin/org/rustSlowTests/RsExternalLinterPassTest.kt
@@ -7,6 +7,7 @@ package org.rustSlowTests
 
 import com.intellij.lang.annotation.HighlightSeverity
 import org.intellij.lang.annotations.Language
+import org.rust.*
 import org.rust.cargo.RsWithToolchainTestBase
 import org.rust.cargo.project.model.cargoProjects
 import org.rust.cargo.project.settings.rustSettings
@@ -14,11 +15,7 @@ import org.rust.cargo.project.workspace.FeatureState
 import org.rust.cargo.project.workspace.PackageFeature
 import org.rust.cargo.project.workspace.PackageOrigin
 import org.rust.cargo.toolchain.ExternalLinter
-import org.rust.fileTree
 import org.rust.ide.annotator.RsExternalLinterUtils
-import org.rust.singleProject
-import org.rust.singleWorkspace
-import org.rust.workspaceOrFail
 
 class RsExternalLinterPassTest : RsWithToolchainTestBase() {
 
@@ -166,6 +163,39 @@ class RsExternalLinterPassTest : RsWithToolchainTestBase() {
         }
     }
 
+    fun `test add reference to index for compiler errors from index`() = checkTooltip("""
+        fn main() {
+            let _: () = 0;
+        }
+    """, """
+        mismatched types <a href="<a href='https://doc.rust-lang.org/error-index.html#E0308'>https://doc.rust-lang.org/error-index.html#E0308</a>">E0308</a><br>expected `()`, found integer
+    """)
+
+    @MinRustcVersion("1.44.1")
+    fun `test don't add reference to index for compiler errors not from index`() = checkTooltip("""
+        fn main() {
+            let x = ();
+        }
+    """, """
+        unused variable: `x`
+        Note: `#[warn(unused_variables)]` on by default
+        Help: if this is intentional, prefix it with an underscore
+    """)
+
+    @MinRustcVersion("1.45.0")
+    fun `test handle hyperlinks in errors`() = checkTooltip("""
+        #![deny(clippy::unwrap_used)]
+        fn main() {
+            let x = Some(());
+            x.unwrap()
+        }
+    """, """
+        used `unwrap()` on `an Option` value
+        Note: the lint level is defined here
+        Help: if you don't want to handle the `None` case gracefully, consider using `expect()` to provide a better panic message
+        Help: for further information visit <a href='https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used'>https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used</a>
+    """, externalLinter = ExternalLinter.CLIPPY)
+
     private fun doTest(
         @Language("Rust") mainRs: String,
         externalLinter: ExternalLinter = ExternalLinter.DEFAULT
@@ -194,4 +224,29 @@ class RsExternalLinterPassTest : RsWithToolchainTestBase() {
         myFixture.openFileInEditor(cargoProjectDirectory.findFileByRelativePath("src/main.rs")!!)
         myFixture.checkHighlighting()
     }
+
+    // BACKCOMPAT: 2021.1. Use `tooltip` attribute instead
+    private fun checkTooltip(
+        @Language("Rust") mainRs: String,
+        tooltip: String,
+        externalLinter: ExternalLinter = ExternalLinter.DEFAULT
+    ) {
+        project.rustSettings.modifyTemporary(testRootDisposable) { it.externalLinter = externalLinter }
+        fileTree {
+            toml("Cargo.toml", """
+                [package]
+                name = "hello"
+                version = "0.1.0"
+                authors = []
+            """)
+
+            dir("src") {
+                file("main.rs", mainRs)
+            }
+        }.create()
+        myFixture.openFileInEditor(cargoProjectDirectory.findFileByRelativePath("src/main.rs")!!)
+        val highlight = myFixture.doHighlighting(HighlightSeverity.WEAK_WARNING)
+            .single { it.description == RsExternalLinterUtils.TEST_MESSAGE }
+        assertEquals(tooltip.trimIndent().replace("\\n", "<br>"), highlight.toolTip)
+    }
 }