diff --git a/libnixf/src/Sema/VariableLookup.cpp b/libnixf/src/Sema/VariableLookup.cpp index e9132d225..21ea86cf0 100644 --- a/libnixf/src/Sema/VariableLookup.cpp +++ b/libnixf/src/Sema/VariableLookup.cpp @@ -125,6 +125,39 @@ void VariableLookupAnalysis::emitEnvLivenessWarning( Diagnostic &D = Diags.emplace_back(Kind, Def->syntax()->range()); D << Name; D.tag(DiagnosticTag::Faded); + + // Add fix for unused let bindings + if (Def->source() == Definition::DS_Let) { + const Node *LetNode = NewEnv->syntax(); + if (LetNode && LetNode->kind() == Node::NK_ExprLet) { + const auto &Let = static_cast(*LetNode); + if (Let.binds()) { + for (const auto &BindNode : Let.binds()->bindings()) { + // Skip non-Binding nodes (Inherit handled separately) + if (BindNode->kind() != Node::NK_Binding) + continue; + + const auto &Bind = static_cast(*BindNode); + const auto &PathNames = Bind.path().names(); + if (PathNames.empty()) + continue; + + const auto &FirstName = PathNames[0]; + if (!FirstName) + continue; + + // Match by comparing first attrname range with Def->syntax() + // range + if (D.range().lCur() == FirstName->range().lCur() && + D.range().rCur() == FirstName->range().rCur()) { + D.fix("remove unused binding") + .edit(TextEdit::mkRemoval(Bind.range())); + break; + } + } + } + } + } } } } diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 9158df891..6e737116f 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -47,6 +47,16 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, if (!Range.overlap(DRange)) continue; + // Determine if this diagnostic's fixes should be preferred + bool IsPreferred = false; + switch (D.kind()) { + case nixf::Diagnostic::DK_UnusedDefLet: + IsPreferred = true; + break; + default: + break; + } + // Add fixes. for (const nixf::Fix &F : D.fixes()) { std::vector Edits; @@ -64,6 +74,7 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, Actions.emplace_back(CodeAction{ .title = F.message(), .kind = std::string(CodeAction::QUICKFIX_KIND), + .isPreferred = IsPreferred, .edit = std::move(WE), }); } diff --git a/nixd/tools/nixd/test/code-action/attr-name/unquote-let-invalid.md b/nixd/tools/nixd/test/code-action/attr-name/unquote-let-invalid.md index 6212eb20d..3f5713ece 100644 --- a/nixd/tools/nixd/test/code-action/attr-name/unquote-let-invalid.md +++ b/nixd/tools/nixd/test/code-action/attr-name/unquote-let-invalid.md @@ -56,12 +56,15 @@ let "123" = 1; in x } ``` -Invalid identifiers (starting with digit) should return empty result +Invalid identifiers (starting with digit) should NOT offer unquote action. +However, a quickfix for unused binding may still be offered. ``` CHECK: "id": 2, CHECK-NEXT: "jsonrpc": "2.0", -CHECK-NEXT: "result": [] +CHECK-NEXT: "result": [ +CHECK-NOT: "title": "unquote attribute name" + CHECK: "title": "remove unused binding" ``` ```json diff --git a/nixd/tools/nixd/test/code-action/attr-name/unquote-let-keyword.md b/nixd/tools/nixd/test/code-action/attr-name/unquote-let-keyword.md index fc2aafc00..a8bff84a3 100644 --- a/nixd/tools/nixd/test/code-action/attr-name/unquote-let-keyword.md +++ b/nixd/tools/nixd/test/code-action/attr-name/unquote-let-keyword.md @@ -56,12 +56,15 @@ let "true" = 1; in x } ``` -Keywords should return empty result (no unquote action available) +Keywords should NOT offer unquote action. +However, a quickfix for unused binding may still be offered. ``` CHECK: "id": 2, CHECK-NEXT: "jsonrpc": "2.0", -CHECK-NEXT: "result": [] +CHECK-NEXT: "result": [ +CHECK-NOT: "title": "unquote attribute name" + CHECK: "title": "remove unused binding" ``` ```json diff --git a/nixd/tools/nixd/test/code-action/unused-def/all-unused.md b/nixd/tools/nixd/test/code-action/unused-def/all-unused.md new file mode 100644 index 000000000..62a5722ba --- /dev/null +++ b/nixd/tools/nixd/test/code-action/unused-def/all-unused.md @@ -0,0 +1,78 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test `remove unused binding` action when all bindings are unused. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///all-unused.nix +let x = 1; y = 2; in 3 +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///all-unused.nix" + }, + "range":{ + "start":{ + "line": 0, + "character": 4 + }, + "end":{ + "line":0, + "character": 5 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The action should offer to remove `x = 1;`. + +``` + CHECK: "id": 2, + CHECK: "newText": "", + CHECK: "range": + CHECK: "end": + CHECK: "character": 10, + CHECK: "line": 0 + CHECK: "start": + CHECK: "character": 4, + CHECK: "line": 0 + CHECK: "isPreferred": true, + CHECK: "kind": "quickfix", + CHECK: "title": "remove unused binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/unused-def/basic.md b/nixd/tools/nixd/test/code-action/unused-def/basic.md new file mode 100644 index 000000000..255572512 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/unused-def/basic.md @@ -0,0 +1,78 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test basic `remove unused binding` action for unused let binding. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///unused-let.nix +let unused = 1; in 2 +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///unused-let.nix" + }, + "range":{ + "start":{ + "line": 0, + "character": 4 + }, + "end":{ + "line":0, + "character": 10 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The action should remove `unused = 1;` from the let expression. + +``` + CHECK: "id": 2, + CHECK: "newText": "", + CHECK: "range": + CHECK: "end": + CHECK: "character": 15, + CHECK: "line": 0 + CHECK: "start": + CHECK: "character": 4, + CHECK: "line": 0 + CHECK: "isPreferred": true, + CHECK: "kind": "quickfix", + CHECK: "title": "remove unused binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/unused-def/multiline.md b/nixd/tools/nixd/test/code-action/unused-def/multiline.md new file mode 100644 index 000000000..48ac08fa1 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/unused-def/multiline.md @@ -0,0 +1,79 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test `remove unused binding` action with multiline let expression. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///multiline.nix +let + unused = 1; +in + 2 +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///multiline.nix" + }, + "range":{ + "start":{ + "line": 1, + "character": 2 + }, + "end":{ + "line":1, + "character": 8 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The action should remove the entire binding line. + +``` + CHECK: "id": 2, + CHECK: "newText": "", + CHECK: "range": + CHECK: "end": + CHECK: "line": 1 + CHECK: "start": + CHECK: "line": 1 + CHECK: "isPreferred": true, + CHECK: "kind": "quickfix", + CHECK: "title": "remove unused binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/unused-def/multiple-bindings.md b/nixd/tools/nixd/test/code-action/unused-def/multiple-bindings.md new file mode 100644 index 000000000..fd541dfb9 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/unused-def/multiple-bindings.md @@ -0,0 +1,78 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +Test `remove unused binding` action with multiple bindings where only one is unused. + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///multiple-bindings.nix +let x = 1; y = 2; in y +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///multiple-bindings.nix" + }, + "range":{ + "start":{ + "line": 0, + "character": 4 + }, + "end":{ + "line":0, + "character": 5 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +The action should remove only `x = 1;` leaving `let y = 2; in y`. + +``` + CHECK: "id": 2, + CHECK: "newText": "", + CHECK: "range": + CHECK: "end": + CHECK: "character": 10, + CHECK: "line": 0 + CHECK: "start": + CHECK: "character": 4, + CHECK: "line": 0 + CHECK: "isPreferred": true, + CHECK: "kind": "quickfix", + CHECK: "title": "remove unused binding" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +```