Skip to content

Commit 0afb329

Browse files
authored
feat: load issue HTML on demand (#703)
1 parent c08995e commit 0afb329

20 files changed

+308
-212
lines changed

README.md

+4-6
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,6 @@ Right now the language server supports the following actions:
130130
},
131131
}
132132
],
133-
"exampleCommitFixes": [
134-
{
135-
"commit": "commit",
136-
"diff": "diff"
137-
}
138-
],
139133
"cwe": "cwe",
140134
"isSecurityType": true
141135
}
@@ -412,6 +406,10 @@ Right now the language server supports the following actions:
412406
- args:
413407
- `folderUri` string,
414408
- `cacheType` `persisted` or `inMemory`
409+
- `Generate Issue Description` Generates issue description in HTML.
410+
- command: `snyk.generateIssueDescription`
411+
- args:
412+
- `issueId` string
415413

416414
## Installation
417415

application/server/server.go

+1
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func {
297297
types.CodeFixDiffsCommand,
298298
types.ExecuteCLICommand,
299299
types.ClearCacheCommand,
300+
types.GenerateIssueDescriptionCommand,
300301
},
301302
},
302303
},

domain/ide/command/clear_cache.go

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ func (cmd *clearCache) purgeInMemoryCache(logger *zerolog.Logger, folderUri *lsp
7575
}
7676
logger.Info().Msgf("deleting in-memory cache for folder %s", folder.Path())
7777
folder.Clear()
78+
if config.CurrentConfig().IsAutoScanEnabled() {
79+
folder.ScanFolder(context.Background())
80+
}
7881
}
7982
}
8083

domain/ide/command/command_factory.go

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func CreateFromCommandData(
9191
return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil
9292
case types.ClearCacheCommand:
9393
return &clearCache{command: commandData}, nil
94+
case types.GenerateIssueDescriptionCommand:
95+
return &generateIssueDescription{command: commandData, issueProvider: issueProvider}, nil
9496
}
9597

9698
return nil, fmt.Errorf("unknown command %v", commandData)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* © 2024 Snyk Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package command
18+
19+
import (
20+
"context"
21+
"errors"
22+
"github.com/rs/zerolog"
23+
"github.com/snyk/snyk-ls/application/config"
24+
"github.com/snyk/snyk-ls/domain/snyk"
25+
"github.com/snyk/snyk-ls/infrastructure/code"
26+
"github.com/snyk/snyk-ls/infrastructure/iac"
27+
"github.com/snyk/snyk-ls/infrastructure/oss"
28+
"github.com/snyk/snyk-ls/internal/product"
29+
"github.com/snyk/snyk-ls/internal/types"
30+
)
31+
32+
type generateIssueDescription struct {
33+
command types.CommandData
34+
issueProvider snyk.IssueProvider
35+
}
36+
37+
func (cmd *generateIssueDescription) Command() types.CommandData {
38+
return cmd.command
39+
}
40+
41+
func (cmd *generateIssueDescription) Execute(_ context.Context) (any, error) {
42+
c := config.CurrentConfig()
43+
logger := c.Logger().With().Str("method", "generateIssueDescription.Execute").Logger()
44+
args := cmd.command.Arguments
45+
46+
issueId, ok := args[0].(string)
47+
if !ok {
48+
return nil, errors.New("failed to parse issue id")
49+
}
50+
51+
issue := cmd.issueProvider.Issue(issueId)
52+
if issue.ID == "" {
53+
return nil, errors.New("failed to find issue")
54+
}
55+
56+
if issue.Product == product.ProductInfrastructureAsCode {
57+
return getIacHtml(c, logger, issue)
58+
} else if issue.Product == product.ProductCode {
59+
return getCodeHtml(c, logger, issue)
60+
} else if issue.Product == product.ProductOpenSource {
61+
return getOssHtml(c, logger, issue)
62+
}
63+
64+
return nil, nil
65+
}
66+
67+
func getOssHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
68+
htmlRender, err := oss.NewHtmlRenderer(c)
69+
if err != nil {
70+
logger.Err(err).Msg("Cannot create Oss HTML render")
71+
return "", err
72+
}
73+
html := htmlRender.GetDetailsHtml(issue)
74+
return html, nil
75+
}
76+
77+
func getCodeHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
78+
htmlRender, err := code.NewHtmlRenderer(c)
79+
if err != nil {
80+
logger.Err(err).Msg("Cannot create Code HTML render")
81+
return "", err
82+
}
83+
html := htmlRender.GetDetailsHtml(issue)
84+
return html, nil
85+
}
86+
87+
func getIacHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
88+
htmlRender, err := iac.NewHtmlRenderer(c)
89+
if err != nil {
90+
logger.Err(err).Msg("Cannot create IaC HTML render")
91+
return "", err
92+
}
93+
html := htmlRender.GetDetailsHtml(issue)
94+
return html, nil
95+
}

domain/ide/converter/converter.go

+24-45
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue {
235235
IsUpgradable: matchingIssue.IsUpgradable,
236236
ProjectName: matchingIssue.ProjectName,
237237
DisplayTargetFile: matchingIssue.DisplayTargetFile,
238-
Details: matchingIssue.Details,
239238
}
240239
}
241240

@@ -272,7 +271,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue {
272271
IsUpgradable: additionalData.IsUpgradable,
273272
ProjectName: additionalData.ProjectName,
274273
DisplayTargetFile: additionalData.DisplayTargetFile,
275-
Details: additionalData.Details,
276274
MatchingIssues: matchingIssues,
277275
Lesson: additionalData.Lesson,
278276
},
@@ -297,16 +295,15 @@ func getIacIssue(issue snyk.Issue) types.ScanIssue {
297295
IsNew: issue.IsNew,
298296
FilterableIssueType: additionalData.GetFilterableIssueType(),
299297
AdditionalData: types.IacIssueData{
300-
Key: additionalData.Key,
301-
PublicId: additionalData.PublicId,
302-
Documentation: additionalData.Documentation,
303-
LineNumber: additionalData.LineNumber,
304-
Issue: additionalData.Issue,
305-
Impact: additionalData.Impact,
306-
Resolve: additionalData.Resolve,
307-
Path: additionalData.Path,
308-
References: additionalData.References,
309-
CustomUIContent: additionalData.CustomUIContent,
298+
Key: additionalData.Key,
299+
PublicId: additionalData.PublicId,
300+
Documentation: additionalData.Documentation,
301+
LineNumber: additionalData.LineNumber,
302+
Issue: additionalData.Issue,
303+
Impact: additionalData.Impact,
304+
Resolve: additionalData.Resolve,
305+
Path: additionalData.Path,
306+
References: additionalData.References,
310307
},
311308
}
312309

@@ -319,22 +316,6 @@ func getCodeIssue(issue snyk.Issue) types.ScanIssue {
319316
return types.ScanIssue{}
320317
}
321318

322-
exampleCommitFixes := make([]types.ExampleCommitFix, 0, len(additionalData.ExampleCommitFixes))
323-
for i := range additionalData.ExampleCommitFixes {
324-
lines := make([]types.CommitChangeLine, 0, len(additionalData.ExampleCommitFixes[i].Lines))
325-
for j := range additionalData.ExampleCommitFixes[i].Lines {
326-
lines = append(lines, types.CommitChangeLine{
327-
Line: additionalData.ExampleCommitFixes[i].Lines[j].Line,
328-
LineNumber: additionalData.ExampleCommitFixes[i].Lines[j].LineNumber,
329-
LineChange: additionalData.ExampleCommitFixes[i].Lines[j].LineChange,
330-
})
331-
}
332-
exampleCommitFixes = append(exampleCommitFixes, types.ExampleCommitFix{
333-
CommitURL: additionalData.ExampleCommitFixes[i].CommitURL,
334-
Lines: lines,
335-
})
336-
}
337-
338319
markers := make([]types.Marker, 0, len(additionalData.Markers))
339320
for _, marker := range additionalData.Markers {
340321
positions := make([]types.MarkerPosition, 0)
@@ -374,23 +355,21 @@ func getCodeIssue(issue snyk.Issue) types.ScanIssue {
374355
IsNew: issue.IsNew,
375356
FilterableIssueType: additionalData.GetFilterableIssueType(),
376357
AdditionalData: types.CodeIssueData{
377-
Key: additionalData.Key,
378-
Message: additionalData.Message,
379-
Rule: additionalData.Rule,
380-
RuleId: additionalData.RuleId,
381-
RepoDatasetSize: additionalData.RepoDatasetSize,
382-
ExampleCommitFixes: exampleCommitFixes,
383-
CWE: additionalData.CWE,
384-
IsSecurityType: additionalData.IsSecurityType,
385-
Text: additionalData.Text,
386-
Cols: additionalData.Cols,
387-
Rows: additionalData.Rows,
388-
PriorityScore: additionalData.PriorityScore,
389-
Markers: markers,
390-
LeadURL: "",
391-
HasAIFix: additionalData.HasAIFix,
392-
DataFlow: dataFlow,
393-
Details: additionalData.Details,
358+
Key: additionalData.Key,
359+
Message: additionalData.Message,
360+
Rule: additionalData.Rule,
361+
RuleId: additionalData.RuleId,
362+
RepoDatasetSize: additionalData.RepoDatasetSize,
363+
CWE: additionalData.CWE,
364+
IsSecurityType: additionalData.IsSecurityType,
365+
Text: additionalData.Text,
366+
Cols: additionalData.Cols,
367+
Rows: additionalData.Rows,
368+
PriorityScore: additionalData.PriorityScore,
369+
Markers: markers,
370+
LeadURL: "",
371+
HasAIFix: additionalData.HasAIFix,
372+
DataFlow: dataFlow,
394373
},
395374
}
396375
if scanIssue.IsIgnored {

infrastructure/code/code.go

-2
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,6 @@ func (sc *Scanner) enhanceIssuesDetails(issues []snyk.Issue, folderPath string)
264264
} else if lesson != nil && lesson.Url != "" {
265265
issue.LessonUrl = lesson.Url
266266
}
267-
268-
issueData.Details = getCodeDetailsHtml(*issue, folderPath)
269267
issue.AdditionalData = issueData
270268
}
271269
}

infrastructure/code/code_html.go

+39-15
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
_ "embed"
2222
"encoding/json"
2323
"fmt"
24+
"github.com/rs/zerolog"
25+
"github.com/snyk/snyk-ls/domain/ide/workspace"
26+
"github.com/snyk/snyk-ls/internal/uri"
2427
"html/template"
2528
"path/filepath"
2629
"regexp"
@@ -60,32 +63,53 @@ type ExampleCommit struct {
6063
//go:embed template/details.html
6164
var detailsHtmlTemplate string
6265

63-
var globalTemplate *template.Template
66+
type HtmlRenderer struct {
67+
c *config.Config
68+
globalTemplate *template.Template
69+
}
6470

65-
func init() {
71+
func NewHtmlRenderer(c *config.Config) (*HtmlRenderer, error) {
6672
funcMap := template.FuncMap{
6773
"repoName": getRepoName,
6874
"trimCWEPrefix": html.TrimCWEPrefix,
6975
"idxMinusOne": html.IdxMinusOne,
7076
}
7177

72-
var err error
73-
globalTemplate, err = template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate)
78+
globalTemplate, err := template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate)
7479
if err != nil {
75-
config.CurrentConfig().Logger().Error().Msgf("Failed to parse details template: %s", err)
80+
c.Logger().Error().Msgf("Failed to parse details template: %s", err)
81+
return nil, err
7682
}
83+
84+
return &HtmlRenderer{
85+
c: c,
86+
globalTemplate: globalTemplate,
87+
}, nil
7788
}
7889

79-
func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
80-
c := config.CurrentConfig()
90+
func determineFolderPath(filePath string) string {
91+
ws := workspace.Get()
92+
if ws == nil {
93+
return ""
94+
}
95+
for _, folder := range ws.Folders() {
96+
folderPath := folder.Path()
97+
if uri.FolderContains(folderPath, filePath) {
98+
return folderPath
99+
}
100+
}
101+
return ""
102+
}
103+
104+
func (renderer *HtmlRenderer) GetDetailsHtml(issue snyk.Issue) string {
81105
additionalData, ok := issue.AdditionalData.(snyk.CodeIssueData)
82106
if !ok {
83-
c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData")
107+
renderer.c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData")
84108
return ""
85109
}
86-
110+
folderPath := determineFolderPath(issue.AffectedFilePath)
87111
exampleCommits := prepareExampleCommits(additionalData.ExampleCommitFixes)
88-
commitFixes := parseExampleCommitsToTemplateJS(exampleCommits)
112+
commitFixes := parseExampleCommitsToTemplateJS(exampleCommits, renderer.c.Logger())
89113

90114
data := map[string]interface{}{
91115
"IssueTitle": additionalData.Title,
@@ -102,7 +126,7 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
102126
"ExampleCommitFixes": exampleCommits,
103127
"CommitFixes": commitFixes,
104128
"PriorityScore": additionalData.PriorityScore,
105-
"SnykWebUrl": config.CurrentConfig().SnykUI(),
129+
"SnykWebUrl": renderer.c.SnykUI(),
106130
"LessonUrl": issue.LessonUrl,
107131
"LessonIcon": html.LessonIcon(),
108132
"IgnoreLineAction": getLineToIgnoreAction(issue),
@@ -126,8 +150,8 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
126150
}
127151

128152
var buffer bytes.Buffer
129-
if err := globalTemplate.Execute(&buffer, data); err != nil {
130-
c.Logger().Error().Msgf("Failed to execute main details template: %v", err)
153+
if err := renderer.globalTemplate.Execute(&buffer, data); err != nil {
154+
renderer.c.Logger().Error().Msgf("Failed to execute main details template: %v", err)
131155
return ""
132156
}
133157

@@ -215,10 +239,10 @@ func prepareExampleCommits(fixes []snyk.ExampleCommitFix) []ExampleCommit {
215239
return fixData
216240
}
217241

218-
func parseExampleCommitsToTemplateJS(fixes []ExampleCommit) template.JS {
242+
func parseExampleCommitsToTemplateJS(fixes []ExampleCommit, logger *zerolog.Logger) template.JS {
219243
jsonFixes, err := json.Marshal(fixes)
220244
if err != nil {
221-
config.CurrentConfig().Logger().Error().Msgf("Failed to marshal example commit fixes: %v", err)
245+
logger.Error().Msgf("Failed to marshal example commit fixes: %v", err)
222246
return ""
223247
}
224248
return template.JS(jsonFixes)

0 commit comments

Comments
 (0)