Skip to content

Commit

Permalink
feat: load issue HTML on demand (#703)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShawkyZ authored Oct 16, 2024
1 parent c08995e commit 0afb329
Show file tree
Hide file tree
Showing 20 changed files with 308 additions and 212 deletions.
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

0 comments on commit 0afb329

Please sign in to comment.