Repository: compiler-explorer/compiler-explorer
Pull request #7357 (2025-02-02 20:57:54)

[rust] Add quickfixes for missing imports and feature flags
The Rust compiler emits error messages that are (in some cases) detailed enough that they can be parsed to generate quickfixes, e. g. when missing an import or a nightly feature flag.

Example: https://godbolt.org/z/jh3bY19P4 With this PR, hitting `Ctrl-.` with the cursor above any of the two errors in the editor will open a quickfix popup that can fix the respective error:

![import_quickfix](https://github.com/user-attachments/assets/0271f6d8-5c6c-4528-ac37-8e87e5699d21)
![feature_quickfix](https://github.com/user-attachments/assets/8842139b-5a4d-4a13-a472-75b27ebd46ab)
=>
![quickfixes_applied](https://github.com/user-attachments/assets/bc994222-fe89-4ac6-9ce4-c1afee2e2354)



## Alternatives

This feature could also be implemented (more robustly) by using `rustc`’s JSON output. However, `rustc` can’t output *both* human-readable and structured diagnostics, so it would be necessary to invoke the compiler twice to do this. Considering that the Rust Playground also parses the normal human-readable error format, I’d argue that the regex-based approach is good enough.

---

Requested in #5284.


Patch:
diff --git a/lib/utils.ts b/lib/utils.ts
index a92f70470..e8f5e6948 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -36,7 +36,7 @@ import _ from 'underscore';
 import type {CacheableValue} from '../types/cache.interfaces.js';
 import {BasicExecutionResult, UnprocessedExecResult} from '../types/execution/execution.interfaces.js';
 import {LanguageKey} from '../types/languages.interfaces.js';
-import type {ResultLine} from '../types/resultline/resultline.interfaces.js';
+import type {Fix, ResultLine} from '../types/resultline/resultline.interfaces.js';
 
 const tabsRe = /\t/g;
 const lineRe = /\r?\n/;
@@ -231,28 +231,64 @@ export function parseOutput(
 }
 
 export function parseRustOutput(lines: string, inputFilename?: string, pathPrefix?: string) {
+    const quickfixes: {re: RegExp; makeFix: (match: string[]) => Fix}[] = [
+        {
+            re: / *help: add `#!\[feature\((.*?)\)]`/,
+            makeFix: ([_, featureName]) => ({
+                title: `Add feature flag \`${featureName}\``,
+                edits: [
+                    {
+                        line: 1,
+                        column: 1,
+                        endline: 1,
+                        endcolumn: 1,
+                        text: `#![feature(${featureName})]\n`,
+                    },
+                ],
+            }),
+        },
+        {
+            re: / *(\d+) *\+ ( *)use (.*?);$/,
+            makeFix: ([_, line, indentation, path]) => ({
+                title: `Add import for \`${path}\``,
+                edits: [
+                    {
+                        line: Number.parseInt(line),
+                        column: 1,
+                        endline: Number.parseInt(line),
+                        endcolumn: 1,
+                        text: `${indentation}use ${path};\n`,
+                    },
+                ],
+            }),
+        },
+    ];
+
     const re = /^\s+-->\s+<source>[(:](\d+)(:?,?(\d+):?)?[):]*\s*(.*)/;
     const result: ResultLine[] = [];
+    let currentDiagnostic: ResultLine | undefined;
     eachLine(lines, line => {
         line = _parseOutputLine(line, inputFilename, pathPrefix);
         if (line !== null) {
             const lineObj: ResultLine = {text: line};
-            const match = filterEscapeSequences(line).match(re);
+            const filteredLine = filterEscapeSequences(line);
+            const match = filteredLine.match(re);
 
             if (match) {
                 const line = Number.parseInt(match[1]);
                 const column = Number.parseInt(match[3] || '0');
 
-                const previous = result.pop();
-                if (previous !== undefined) {
-                    const text = filterEscapeSequences(previous.text);
-                    previous.tag = {
+                currentDiagnostic = result.pop();
+                if (currentDiagnostic !== undefined) {
+                    const text = filterEscapeSequences(currentDiagnostic.text);
+                    currentDiagnostic.tag = {
                         line,
                         column,
                         text,
                         severity: parseSeverity(text),
+                        fixes: [],
                     };
-                    result.push(previous);
+                    result.push(currentDiagnostic);
                 }
 
                 lineObj.tag = {
@@ -262,6 +298,16 @@ export function parseRustOutput(lines: string, inputFilename?: string, pathPrefi
                     severity: 3,
                 };
             }
+
+            if (currentDiagnostic?.tag?.fixes !== undefined) {
+                for (const {re, makeFix} of quickfixes) {
+                    const match = filteredLine.match(re);
+                    if (match) {
+                        currentDiagnostic.tag.fixes.push(makeFix(match));
+                    }
+                }
+            }
+
             result.push(lineObj);
         }
     });


Test patch:
diff --git a/test/utils-tests.ts b/test/utils-tests.ts
index db7b6c4c1..6737c335e 100644
--- a/test/utils-tests.ts
+++ b/test/utils-tests.ts
@@ -249,7 +249,7 @@ describe('Rust compiler output', () => {
         expect(utils.parseRustOutput('Unrelated\nLine one\n --> bob.rs:1\nUnrelated', 'bob.rs')).toEqual([
             {text: 'Unrelated'},
             {
-                tag: {column: 0, line: 1, text: 'Line one', severity: 3},
+                tag: {column: 0, line: 1, text: 'Line one', severity: 3, fixes: []},
                 text: 'Line one',
             },
             {
@@ -260,7 +260,7 @@ describe('Rust compiler output', () => {
         ]);
         expect(utils.parseRustOutput('Line one\n --> bob.rs:1:5', 'bob.rs')).toEqual([
             {
-                tag: {column: 5, line: 1, text: 'Line one', severity: 3},
+                tag: {column: 5, line: 1, text: 'Line one', severity: 3, fixes: []},
                 text: 'Line one',
             },
             {
@@ -270,7 +270,7 @@ describe('Rust compiler output', () => {
         ]);
         expect(utils.parseRustOutput('Multiple spaces\n   --> bob.rs:1:5', 'bob.rs')).toEqual([
             {
-                tag: {column: 5, line: 1, text: 'Multiple spaces', severity: 3},
+                tag: {column: 5, line: 1, text: 'Multiple spaces', severity: 3, fixes: []},
                 text: 'Multiple spaces',
             },
             {
@@ -283,7 +283,7 @@ describe('Rust compiler output', () => {
     it('replaces all references to input source', () => {
         expect(utils.parseRustOutput('error: Error in bob.rs\n --> bob.rs:1', 'bob.rs')).toEqual([
             {
-                tag: {column: 0, line: 1, text: 'error: Error in <source>', severity: 3},
+                tag: {column: 0, line: 1, text: 'error: Error in <source>', severity: 3, fixes: []},
                 text: 'error: Error in <source>',
             },
             {
@@ -296,7 +296,7 @@ describe('Rust compiler output', () => {
     it('treats <stdin> as if it were the compiler source', () => {
         expect(utils.parseRustOutput('error: <stdin> is sad\n --> <stdin>:120:25', 'bob.rs')).toEqual([
             {
-                tag: {column: 25, line: 120, text: 'error: <source> is sad', severity: 3},
+                tag: {column: 25, line: 120, text: 'error: <source> is sad', severity: 3, fixes: []},
                 text: 'error: <source> is sad',
             },
             {
@@ -318,6 +318,7 @@ describe('Rust compiler output', () => {
                     column: 27,
                     text: 'error[E0425]: cannot find value `x` in this scope',
                     severity: 3,
+                    fixes: [],
                 },
                 text: 'error[\x1B]8;;https://doc.rust-lang.org/error_codes/E0425.html\x07E0425\x1B]8;;\x07]: cannot find value `x` in this scope',
             },
@@ -332,6 +333,84 @@ describe('Rust compiler output', () => {
             },
         ]);
     });
+
+    it('emits quickfixes', () => {
+        expect(utils.parseRustOutput('error\n --> <source>:42:27\n15  + use std::collections::HashMap;')).toEqual([
+            {
+                tag: {
+                    line: 42,
+                    column: 27,
+                    text: 'error',
+                    severity: 3,
+                    fixes: [
+                        {
+                            title: 'Add import for `std::collections::HashMap`',
+                            edits: [
+                                {
+                                    line: 15,
+                                    column: 1,
+                                    endline: 15,
+                                    endcolumn: 1,
+                                    text: 'use std::collections::HashMap;\n',
+                                },
+                            ],
+                        },
+                    ],
+                },
+                text: 'error',
+            },
+            {
+                tag: {
+                    line: 42,
+                    column: 27,
+                    text: '',
+                    severity: 3,
+                },
+                text: ' --> <source>:42:27',
+            },
+            {text: '15  + use std::collections::HashMap;'},
+        ]);
+
+        expect(
+            utils.parseRustOutput(
+                'error\n --> <source>:42:27\n   = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable',
+            ),
+        ).toEqual([
+            {
+                tag: {
+                    line: 42,
+                    column: 27,
+                    text: 'error',
+                    severity: 3,
+                    fixes: [
+                        {
+                            title: 'Add feature flag `num_midpoint_signed`',
+                            edits: [
+                                {
+                                    line: 1,
+                                    column: 1,
+                                    endline: 1,
+                                    endcolumn: 1,
+                                    text: '#![feature(num_midpoint_signed)]\n',
+                                },
+                            ],
+                        },
+                    ],
+                },
+                text: 'error',
+            },
+            {
+                tag: {
+                    line: 42,
+                    column: 27,
+                    text: '',
+                    severity: 3,
+                },
+                text: ' --> <source>:42:27',
+            },
+            {text: '   = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable'},
+        ]);
+    });
 });
 
 describe('Tool output', () => {