Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: load issue HTML on demand #703

Merged
merged 16 commits into from
Oct 16, 2024
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,6 @@ Right now the language server supports the following actions:
},
}
],
"exampleCommitFixes": [
{
"commit": "commit",
"diff": "diff"
}
],
"cwe": "cwe",
"isSecurityType": true
}
Expand Down Expand Up @@ -412,6 +406,10 @@ Right now the language server supports the following actions:
- args:
- `folderUri` string,
- `cacheType` `persisted` or `inMemory`
- `Generate Issue Description` Generates issue description in HTML.
- command: `snyk.generateIssueDescription`
- args:
- `issueId` string

## Installation

Expand Down
1 change: 1 addition & 0 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func {
types.CodeFixDiffsCommand,
types.ExecuteCLICommand,
types.ClearCacheCommand,
types.GenerateIssueDescriptionCommand,
},
},
},
Expand Down
3 changes: 3 additions & 0 deletions domain/ide/command/clear_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func (cmd *clearCache) purgeInMemoryCache(logger *zerolog.Logger, folderUri *lsp
}
logger.Info().Msgf("deleting in-memory cache for folder %s", folder.Path())
folder.Clear()
if config.CurrentConfig().IsAutoScanEnabled() {
folder.ScanFolder(context.Background())
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions domain/ide/command/command_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func CreateFromCommandData(
return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil
case types.ClearCacheCommand:
return &clearCache{command: commandData}, nil
case types.GenerateIssueDescriptionCommand:
return &generateIssueDescription{command: commandData, issueProvider: issueProvider}, nil
}

return nil, fmt.Errorf("unknown command %v", commandData)
Expand Down
95 changes: 95 additions & 0 deletions domain/ide/command/generate_issue_description.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* © 2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package command

import (
"context"
"errors"
"github.com/rs/zerolog"
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/code"
"github.com/snyk/snyk-ls/infrastructure/iac"
"github.com/snyk/snyk-ls/infrastructure/oss"
"github.com/snyk/snyk-ls/internal/product"
"github.com/snyk/snyk-ls/internal/types"
)

type generateIssueDescription struct {
command types.CommandData
issueProvider snyk.IssueProvider
}

func (cmd *generateIssueDescription) Command() types.CommandData {
return cmd.command
}

func (cmd *generateIssueDescription) Execute(_ context.Context) (any, error) {
c := config.CurrentConfig()
logger := c.Logger().With().Str("method", "generateIssueDescription.Execute").Logger()
args := cmd.command.Arguments

issueId, ok := args[0].(string)
if !ok {
return nil, errors.New("failed to parse issue id")
}

issue := cmd.issueProvider.Issue(issueId)
if issue.ID == "" {
return nil, errors.New("failed to find issue")
}

if issue.Product == product.ProductInfrastructureAsCode {
return getIacHtml(c, logger, issue)
} else if issue.Product == product.ProductCode {
return getCodeHtml(c, logger, issue)
} else if issue.Product == product.ProductOpenSource {
return getOssHtml(c, logger, issue)
}

return nil, nil
}

func getOssHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
htmlRender, err := oss.NewHtmlRenderer(c)
if err != nil {
logger.Err(err).Msg("Cannot create Oss HTML render")
return "", err
}
html := htmlRender.GetDetailsHtml(issue)
return html, nil
}

func getCodeHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
htmlRender, err := code.NewHtmlRenderer(c)
if err != nil {
logger.Err(err).Msg("Cannot create Code HTML render")
return "", err
}
html := htmlRender.GetDetailsHtml(issue)
return html, nil
}

func getIacHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) {
htmlRender, err := iac.NewHtmlRenderer(c)
if err != nil {
logger.Err(err).Msg("Cannot create IaC HTML render")
return "", err
}
html := htmlRender.GetDetailsHtml(issue)
return html, nil
}
69 changes: 24 additions & 45 deletions domain/ide/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue {
IsUpgradable: matchingIssue.IsUpgradable,
ProjectName: matchingIssue.ProjectName,
DisplayTargetFile: matchingIssue.DisplayTargetFile,
Details: matchingIssue.Details,
}
}

Expand Down Expand Up @@ -272,7 +271,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue {
IsUpgradable: additionalData.IsUpgradable,
ProjectName: additionalData.ProjectName,
DisplayTargetFile: additionalData.DisplayTargetFile,
Details: additionalData.Details,
MatchingIssues: matchingIssues,
Lesson: additionalData.Lesson,
},
Expand All @@ -297,16 +295,15 @@ func getIacIssue(issue snyk.Issue) types.ScanIssue {
IsNew: issue.IsNew,
FilterableIssueType: additionalData.GetFilterableIssueType(),
AdditionalData: types.IacIssueData{
Key: additionalData.Key,
PublicId: additionalData.PublicId,
Documentation: additionalData.Documentation,
LineNumber: additionalData.LineNumber,
Issue: additionalData.Issue,
Impact: additionalData.Impact,
Resolve: additionalData.Resolve,
Path: additionalData.Path,
References: additionalData.References,
CustomUIContent: additionalData.CustomUIContent,
Key: additionalData.Key,
PublicId: additionalData.PublicId,
Documentation: additionalData.Documentation,
LineNumber: additionalData.LineNumber,
Issue: additionalData.Issue,
Impact: additionalData.Impact,
Resolve: additionalData.Resolve,
Path: additionalData.Path,
References: additionalData.References,
},
}

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

exampleCommitFixes := make([]types.ExampleCommitFix, 0, len(additionalData.ExampleCommitFixes))
for i := range additionalData.ExampleCommitFixes {
lines := make([]types.CommitChangeLine, 0, len(additionalData.ExampleCommitFixes[i].Lines))
for j := range additionalData.ExampleCommitFixes[i].Lines {
lines = append(lines, types.CommitChangeLine{
Line: additionalData.ExampleCommitFixes[i].Lines[j].Line,
LineNumber: additionalData.ExampleCommitFixes[i].Lines[j].LineNumber,
LineChange: additionalData.ExampleCommitFixes[i].Lines[j].LineChange,
})
}
exampleCommitFixes = append(exampleCommitFixes, types.ExampleCommitFix{
CommitURL: additionalData.ExampleCommitFixes[i].CommitURL,
Lines: lines,
})
}

markers := make([]types.Marker, 0, len(additionalData.Markers))
for _, marker := range additionalData.Markers {
positions := make([]types.MarkerPosition, 0)
Expand Down Expand Up @@ -374,23 +355,21 @@ func getCodeIssue(issue snyk.Issue) types.ScanIssue {
IsNew: issue.IsNew,
FilterableIssueType: additionalData.GetFilterableIssueType(),
AdditionalData: types.CodeIssueData{
Key: additionalData.Key,
Message: additionalData.Message,
Rule: additionalData.Rule,
RuleId: additionalData.RuleId,
RepoDatasetSize: additionalData.RepoDatasetSize,
ExampleCommitFixes: exampleCommitFixes,
CWE: additionalData.CWE,
IsSecurityType: additionalData.IsSecurityType,
Text: additionalData.Text,
Cols: additionalData.Cols,
Rows: additionalData.Rows,
PriorityScore: additionalData.PriorityScore,
Markers: markers,
LeadURL: "",
HasAIFix: additionalData.HasAIFix,
DataFlow: dataFlow,
Details: additionalData.Details,
Key: additionalData.Key,
Message: additionalData.Message,
Rule: additionalData.Rule,
RuleId: additionalData.RuleId,
RepoDatasetSize: additionalData.RepoDatasetSize,
CWE: additionalData.CWE,
IsSecurityType: additionalData.IsSecurityType,
Text: additionalData.Text,
Cols: additionalData.Cols,
Rows: additionalData.Rows,
PriorityScore: additionalData.PriorityScore,
Markers: markers,
LeadURL: "",
HasAIFix: additionalData.HasAIFix,
DataFlow: dataFlow,
},
}
if scanIssue.IsIgnored {
Expand Down
2 changes: 0 additions & 2 deletions infrastructure/code/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,6 @@ func (sc *Scanner) enhanceIssuesDetails(issues []snyk.Issue, folderPath string)
} else if lesson != nil && lesson.Url != "" {
issue.LessonUrl = lesson.Url
}

issueData.Details = getCodeDetailsHtml(*issue, folderPath)
issue.AdditionalData = issueData
}
}
Expand Down
54 changes: 39 additions & 15 deletions infrastructure/code/code_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
_ "embed"
"encoding/json"
"fmt"
"github.com/rs/zerolog"
"github.com/snyk/snyk-ls/domain/ide/workspace"
"github.com/snyk/snyk-ls/internal/uri"
"html/template"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -60,32 +63,53 @@ type ExampleCommit struct {
//go:embed template/details.html
var detailsHtmlTemplate string

var globalTemplate *template.Template
type HtmlRenderer struct {
c *config.Config
globalTemplate *template.Template
}

func init() {
func NewHtmlRenderer(c *config.Config) (*HtmlRenderer, error) {
funcMap := template.FuncMap{
"repoName": getRepoName,
"trimCWEPrefix": html.TrimCWEPrefix,
"idxMinusOne": html.IdxMinusOne,
}

var err error
globalTemplate, err = template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate)
globalTemplate, err := template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate)
if err != nil {
config.CurrentConfig().Logger().Error().Msgf("Failed to parse details template: %s", err)
c.Logger().Error().Msgf("Failed to parse details template: %s", err)
return nil, err
}

return &HtmlRenderer{
c: c,
globalTemplate: globalTemplate,
}, nil
}

func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
c := config.CurrentConfig()
func determineFolderPath(filePath string) string {
ws := workspace.Get()
if ws == nil {
return ""
}
for _, folder := range ws.Folders() {
folderPath := folder.Path()
if uri.FolderContains(folderPath, filePath) {
return folderPath
}
}
return ""
}

func (renderer *HtmlRenderer) GetDetailsHtml(issue snyk.Issue) string {
additionalData, ok := issue.AdditionalData.(snyk.CodeIssueData)
if !ok {
c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData")
renderer.c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData")
return ""
}

folderPath := determineFolderPath(issue.AffectedFilePath)
exampleCommits := prepareExampleCommits(additionalData.ExampleCommitFixes)
commitFixes := parseExampleCommitsToTemplateJS(exampleCommits)
commitFixes := parseExampleCommitsToTemplateJS(exampleCommits, renderer.c.Logger())

data := map[string]interface{}{
"IssueTitle": additionalData.Title,
Expand All @@ -102,7 +126,7 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
"ExampleCommitFixes": exampleCommits,
"CommitFixes": commitFixes,
"PriorityScore": additionalData.PriorityScore,
"SnykWebUrl": config.CurrentConfig().SnykUI(),
"SnykWebUrl": renderer.c.SnykUI(),
"LessonUrl": issue.LessonUrl,
"LessonIcon": html.LessonIcon(),
"IgnoreLineAction": getLineToIgnoreAction(issue),
Expand All @@ -126,8 +150,8 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string {
}

var buffer bytes.Buffer
if err := globalTemplate.Execute(&buffer, data); err != nil {
c.Logger().Error().Msgf("Failed to execute main details template: %v", err)
if err := renderer.globalTemplate.Execute(&buffer, data); err != nil {
renderer.c.Logger().Error().Msgf("Failed to execute main details template: %v", err)
return ""
}

Expand Down Expand Up @@ -215,10 +239,10 @@ func prepareExampleCommits(fixes []snyk.ExampleCommitFix) []ExampleCommit {
return fixData
}

func parseExampleCommitsToTemplateJS(fixes []ExampleCommit) template.JS {
func parseExampleCommitsToTemplateJS(fixes []ExampleCommit, logger *zerolog.Logger) template.JS {
jsonFixes, err := json.Marshal(fixes)
if err != nil {
config.CurrentConfig().Logger().Error().Msgf("Failed to marshal example commit fixes: %v", err)
logger.Error().Msgf("Failed to marshal example commit fixes: %v", err)
return ""
}
return template.JS(jsonFixes)
Expand Down
Loading