Skip to content

Commit d8776ec

Browse files
refactor: move autofix and autofix feedback logic from language server to code-client-go [IDE-727] (#100)
1 parent 9367609 commit d8776ec

File tree

6 files changed

+318
-16
lines changed

6 files changed

+318
-16
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/go-git/go-git/v5 v5.14.0
77
github.com/golang/mock v1.6.0
88
github.com/google/uuid v1.6.0
9+
github.com/hexops/gotextdiff v1.0.3
910
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
1011
github.com/oapi-codegen/runtime v1.1.1
1112
github.com/pact-foundation/pact-go/v2 v2.0.5

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
8282
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
8383
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
8484
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
85+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
86+
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
8587
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
8688
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
8789
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

llm/api_client.go

Lines changed: 135 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/base64"
77
"encoding/json"
8+
"errors"
89
"io"
910
"net/http"
1011
"net/url"
@@ -40,18 +41,42 @@ func (d *DeepCodeLLMBindingImpl) runExplain(ctx context.Context, options Explain
4041
return Explanations{}, err
4142
}
4243
}
43-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewBuffer(requestBody))
44+
45+
responseBody, err := d.submitRequest(ctx, u, requestBody)
4446
if err != nil {
45-
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request")
4647
return Explanations{}, err
4748
}
4849

50+
var response explainResponse
51+
var explains Explanations
52+
response.Status = completeStatus
53+
err = json.Unmarshal(responseBody, &response)
54+
if err != nil {
55+
logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling")
56+
return Explanations{}, err
57+
}
58+
59+
explains = response.Explanation
60+
61+
return explains, nil
62+
}
63+
64+
func (d *DeepCodeLLMBindingImpl) submitRequest(ctx context.Context, url *url.URL, requestBody []byte) ([]byte, error) {
65+
logger := d.logger.With().Str("method", "submitRequest").Logger()
66+
logger.Trace().Str("payload body: %s\n", string(requestBody)).Msg("Marshaled payload")
67+
68+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewBuffer(requestBody))
69+
if err != nil {
70+
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request")
71+
return nil, err
72+
}
73+
4974
d.addDefaultHeaders(req)
5075

5176
resp, err := d.httpClientFunc().Do(req) //nolint:bodyclose // this seems to be a false positive
5277
if err != nil {
5378
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response")
54-
return Explanations{}, err
79+
return nil, err
5580
}
5681
defer func(Body io.ReadCloser) {
5782
bodyCloseErr := Body.Close()
@@ -64,21 +89,11 @@ func (d *DeepCodeLLMBindingImpl) runExplain(ctx context.Context, options Explain
6489
responseBody, err := io.ReadAll(resp.Body)
6590
if err != nil {
6691
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error reading all response")
67-
return Explanations{}, err
92+
return nil, err
6893
}
6994
logger.Debug().Str("response body: %s\n", string(responseBody)).Msg("Got the response")
70-
var response explainResponse
71-
var explains Explanations
72-
response.Status = completeStatus
73-
err = json.Unmarshal(responseBody, &response)
74-
if err != nil {
75-
logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling")
76-
return Explanations{}, err
77-
}
78-
79-
explains = response.Explanation
8095

81-
return explains, nil
96+
return responseBody, nil
8297
}
8398

8499
func (d *DeepCodeLLMBindingImpl) explainRequestBody(options *ExplainOptions) ([]byte, error) {
@@ -105,6 +120,111 @@ func (d *DeepCodeLLMBindingImpl) explainRequestBody(options *ExplainOptions) ([]
105120
return requestBody, marshalErr
106121
}
107122

123+
var failed = AutofixStatus{Message: "FAILED"}
124+
125+
func (d *DeepCodeLLMBindingImpl) runAutofix(ctx context.Context, requestId string, options AutofixOptions) (AutofixResponse, AutofixStatus, error) {
126+
span := d.instrumentor.StartSpan(ctx, "code.RunAutofix")
127+
defer span.Finish()
128+
129+
logger := d.logger.With().Str("method", "code.RunAutofix").Str("requestId", requestId).Logger()
130+
131+
requestBody, err := d.autofixRequestBody(&options)
132+
if err != nil {
133+
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body")
134+
return AutofixResponse{}, failed, err
135+
}
136+
137+
logger.Info().Msg("Started obtaining autofix Response")
138+
responseBody, err := d.submitRequest(ctx, options.Endpoint, requestBody)
139+
logger.Info().Msg("Finished obtaining autofix Response")
140+
141+
if err != nil {
142+
logger.Err(err).Str("responseBody", string(responseBody)).Msg("error response from autofix")
143+
return AutofixResponse{}, failed, err
144+
}
145+
146+
var response AutofixResponse
147+
err = json.Unmarshal(responseBody, &response)
148+
if err != nil {
149+
logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling")
150+
return AutofixResponse{}, failed, err
151+
}
152+
153+
logger.Debug().Msgf("Status: %s", response.Status)
154+
155+
if response.Status == failed.Message {
156+
errMsg := "autofix failed"
157+
logger.Error().Str("responseStatus", response.Status).Msg(errMsg)
158+
return response, failed, errors.New(errMsg)
159+
}
160+
161+
if response.Status == "" {
162+
errMsg := "unknown response status (empty)"
163+
logger.Error().Str("responseStatus", response.Status).Msg(errMsg)
164+
return response, failed, errors.New(errMsg)
165+
}
166+
167+
status := AutofixStatus{Message: response.Status}
168+
if response.Status != completeStatus {
169+
return response, status, nil
170+
}
171+
172+
return response, status, nil
173+
}
174+
175+
func (d *DeepCodeLLMBindingImpl) autofixRequestBody(options *AutofixOptions) ([]byte, error) {
176+
request := AutofixRequest{
177+
Key: AutofixRequestKey{
178+
Type: "file",
179+
Hash: options.BundleHash,
180+
FilePath: options.FilePath,
181+
RuleId: options.RuleID,
182+
LineNum: options.LineNum,
183+
},
184+
AnalysisContext: options.CodeRequestContext,
185+
IdeExtensionDetails: options.IdeExtensionDetails,
186+
}
187+
if len(options.ShardKey) > 0 {
188+
request.Key.Shard = options.ShardKey
189+
}
190+
191+
requestBody, err := json.Marshal(request)
192+
return requestBody, err
193+
}
194+
195+
func (d *DeepCodeLLMBindingImpl) submitAutofixFeedback(ctx context.Context, requestId string, options AutofixFeedbackOptions) error {
196+
span := d.instrumentor.StartSpan(ctx, "code.SubmitAutofixFeedback")
197+
defer span.Finish()
198+
199+
logger := d.logger.With().Str("method", "code.SubmitAutofixFeedback").Str("requestId", requestId).Logger()
200+
201+
requestBody, err := d.autofixFeedbackRequestBody(&options)
202+
if err != nil {
203+
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body")
204+
return err
205+
}
206+
207+
logger.Info().Msg("Started obtaining autofix Response")
208+
_, err = d.submitRequest(ctx, options.Endpoint, requestBody)
209+
logger.Info().Msg("Finished obtaining autofix Response")
210+
211+
return err
212+
}
213+
214+
func (d *DeepCodeLLMBindingImpl) autofixFeedbackRequestBody(options *AutofixFeedbackOptions) ([]byte, error) {
215+
request := AutofixUserEvent{
216+
Channel: "IDE",
217+
EventType: options.Result,
218+
EventDetails: AutofixEventDetails{FixId: options.FixID},
219+
AnalysisContext: options.CodeRequestContext,
220+
IdeExtensionDetails: options.IdeExtensionDetails,
221+
}
222+
223+
requestBody, err := json.Marshal(request)
224+
225+
return requestBody, err
226+
}
227+
108228
func prepareDiffs(diffs []string) []string {
109229
cleanedDiffs := make([]string, 0, len(diffs))
110230
for _, diff := range diffs {

llm/binding.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,45 @@ type ExplainResult []string
5151
type DeepCodeLLMBinding interface {
5252
SnykLLMBindings
5353
ExplainWithOptions(ctx context.Context, options ExplainOptions) (ExplainResult, error)
54+
GetAutofixDiffs(ctx context.Context, baseDir string, options AutofixOptions) (unifiedDiffSuggestions []AutofixUnifiedDiffSuggestion, status AutofixStatus, err error)
55+
SubmitAutofixFeedback(ctx context.Context, requestId string, options AutofixFeedbackOptions) error
5456
}
5557

5658
// DeepCodeLLMBindingImpl is an LLM binding for the Snyk Code LLM.
57-
// Currently, it only supports explain.
5859
type DeepCodeLLMBindingImpl struct {
5960
httpClientFunc func() http.HTTPClient
6061
logger *zerolog.Logger
6162
outputFormat OutputFormat
6263
instrumentor observability.Instrumentor
6364
}
6465

66+
func (d *DeepCodeLLMBindingImpl) SubmitAutofixFeedback(ctx context.Context, requestId string, options AutofixFeedbackOptions) error {
67+
method := "SubmitAutofixFeedback"
68+
span := d.instrumentor.StartSpan(ctx, method)
69+
defer d.instrumentor.Finish(span)
70+
logger := d.logger.With().Str("method", method).Str("requestId", requestId).Logger()
71+
logger.Info().Msg("Started submitting autofix feedback")
72+
defer logger.Info().Msg("Finished submitting autofix feedback")
73+
74+
err := d.submitAutofixFeedback(ctx, requestId, options)
75+
return err
76+
}
77+
78+
func (d *DeepCodeLLMBindingImpl) GetAutofixDiffs(ctx context.Context, requestId string, options AutofixOptions) (unifiedDiffSuggestions []AutofixUnifiedDiffSuggestion, status AutofixStatus, err error) {
79+
method := "GetAutofixDiffs"
80+
span := d.instrumentor.StartSpan(ctx, method)
81+
defer d.instrumentor.Finish(span)
82+
logger := d.logger.With().Str("method", method).Str("requestId", requestId).Logger()
83+
logger.Info().Msg("Started obtaining autofix diffs")
84+
defer logger.Info().Msg("Finished obtaining autofix diffs")
85+
86+
autofixResponse, status, err := d.runAutofix(ctx, requestId, options)
87+
if err != nil {
88+
return nil, status, err
89+
}
90+
return autofixResponse.toUnifiedDiffSuggestions(d.logger, options.BaseDir, options.FilePath), status, err
91+
}
92+
6593
func (d *DeepCodeLLMBindingImpl) ExplainWithOptions(ctx context.Context, options ExplainOptions) (ExplainResult, error) {
6694
s := d.instrumentor.StartSpan(ctx, "code.ExplainWithOptions")
6795
defer d.instrumentor.Finish(s)

llm/convert.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package llm
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/hexops/gotextdiff"
11+
"github.com/hexops/gotextdiff/myers"
12+
"github.com/hexops/gotextdiff/span"
13+
"github.com/rs/zerolog"
14+
)
15+
16+
func (s *AutofixResponse) toUnifiedDiffSuggestions(logger *zerolog.Logger, baseDir string, filePath string) []AutofixUnifiedDiffSuggestion {
17+
var fixSuggestions []AutofixUnifiedDiffSuggestion
18+
for _, suggestion := range s.AutofixSuggestions {
19+
decodedPath, unifiedDiff := getPathAndUnifiedDiff(logger, baseDir, filePath, suggestion.Value)
20+
if decodedPath == "" || unifiedDiff == "" {
21+
continue
22+
}
23+
24+
d := AutofixUnifiedDiffSuggestion{
25+
FixId: suggestion.Id,
26+
UnifiedDiffsPerFile: map[string]string{},
27+
}
28+
29+
d.UnifiedDiffsPerFile[decodedPath] = unifiedDiff
30+
fixSuggestions = append(fixSuggestions, d)
31+
}
32+
return fixSuggestions
33+
}
34+
35+
func getPathAndUnifiedDiff(zeroLogger *zerolog.Logger, baseDir string, filePath string, newText string) (decodedPath string, unifiedDiff string) {
36+
logger := zeroLogger.With().Str("method", "getUnifiedDiff").Logger()
37+
38+
decodedPath, err := url.PathUnescape(filepath.Join(baseDir, filePath))
39+
if err != nil {
40+
logger.Err(err).Msgf("cannot decode filePath %s", filePath)
41+
return
42+
}
43+
logger.Debug().Msgf("File decodedPath %s", decodedPath)
44+
45+
fileContent, err := os.ReadFile(decodedPath)
46+
if err != nil {
47+
logger.Err(err).Msgf("cannot read fileContent %s", decodedPath)
48+
return
49+
}
50+
51+
// Workaround: AI Suggestion API only returns \n new lines. It doesn't consider carriage returns.
52+
contentBefore := strings.Replace(string(fileContent), "\r\n", "\n", -1)
53+
edits := myers.ComputeEdits(span.URIFromPath(decodedPath), contentBefore, newText)
54+
unifiedDiff = fmt.Sprint(gotextdiff.ToUnified(decodedPath, decodedPath+"fixed", contentBefore, edits))
55+
56+
return decodedPath, unifiedDiff
57+
}

0 commit comments

Comments
 (0)