Skip to content

Commit

Permalink
Add support to analyze studio projects
Browse files Browse the repository at this point in the history
  • Loading branch information
thschmitt committed Dec 18, 2024
1 parent c0500be commit cbd4e36
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 43 deletions.
6 changes: 1 addition & 5 deletions cache/file_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
)

const cacheFilePermissions = 0600
const cacheDirectoryPermissions = 0700
const separator = "|"

// The FileCache stores data on disk in order to preserve them across
Expand Down Expand Up @@ -68,12 +67,9 @@ func (c FileCache) cacheFilePath(key string) (string, error) {
if err != nil {
return "", err
}
fileCacheDirectory := filepath.Join(cacheDirectory, "cache")
_ = os.MkdirAll(fileCacheDirectory, cacheDirectoryPermissions)

hash := sha256.Sum256([]byte(key))
fileName := fmt.Sprintf("%x", hash)
return filepath.Join(fileCacheDirectory, fileName), nil
return filepath.Join(cacheDirectory, fileName), nil
}

func NewFileCache() *FileCache {
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func main() {
plugin_orchestrator.NewUploadCommand(),
plugin_orchestrator.NewDownloadCommand(),
plugin_studio.NewPackagePackCommand(),
plugin_studio.NewPackageAnalyzeCommand(),
},
),
*configProvider,
Expand Down
9 changes: 4 additions & 5 deletions plugin/external_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type ExternalPlugin struct {
}

func (p ExternalPlugin) GetTool(name string, url string, executable string) (string, error) {
pluginDirectory, err := p.cacheFilePath(name, url)
pluginDirectory, err := p.pluginDirectory(name, url)
if err != nil {
return "", fmt.Errorf("Could not download %s: %v", name, err)
}
Expand Down Expand Up @@ -88,15 +88,14 @@ func (p ExternalPlugin) progressReader(text string, completedText string, reader
return progressReader
}

func (p ExternalPlugin) cacheFilePath(name string, url string) (string, error) {
cacheDirectory, err := utils.Directories{}.Cache()
func (p ExternalPlugin) pluginDirectory(name string, url string) (string, error) {
pluginDirectory, err := utils.Directories{}.Plugin()
if err != nil {
return "", err
}
hash := sha256.Sum256([]byte(url))
subdirectory := fmt.Sprintf("%s-%x", name, hash)
pluginDirectory := filepath.Join(cacheDirectory, "plugins", subdirectory)
return pluginDirectory, nil
return filepath.Join(pluginDirectory, subdirectory), nil
}

func (p ExternalPlugin) randomFolderName() string {
Expand Down
25 changes: 25 additions & 0 deletions plugin/studio/analyze_result_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package studio

type analyzeResultJson []struct {
ErrorCode string `json:"ErrorCode"`
Description string `json:"Description"`
RuleName string `json:"RuleName"`
FilePath string `json:"FilePath"`
ActivityId *analyzeResultActivityId `json:"ActivityId"`
ActivityDisplayName string `json:"ActivityDisplayName"`
WorkflowDisplayName string `json:"WorkflowDisplayName"`
Item *analyzeResultItem `json:"Item"`
ErrorSeverity int `json:"ErrorSeverity"`
Recommendation string `json:"Recommendation"`
DocumentationLink string `json:"DocumentationLink"`
}

type analyzeResultActivityId struct {
Id string `json:"Id"`
IdRef string `json:"IdRef"`
}

type analyzeResultItem struct {
Name string `json:"Name"`
Type int `json:"Type"`
}
273 changes: 273 additions & 0 deletions plugin/studio/package_analyze_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
package studio

import (
"bufio"
"bytes"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"math/big"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/UiPath/uipathcli/log"
"github.com/UiPath/uipathcli/output"
"github.com/UiPath/uipathcli/plugin"
"github.com/UiPath/uipathcli/utils"
)

// The PackageAnalyzeCommand runs static code analyis on the project to detect common errors.
type PackageAnalyzeCommand struct {
Exec utils.ExecProcess
}

func (c PackageAnalyzeCommand) Command() plugin.Command {
return *plugin.NewCommand("studio").
WithCategory("package", "Package", "UiPath Studio package-related actions").
WithOperation("analyze", "Analyze Project", "Runs static code analysis on the project to detect common errors").
WithParameter("source", plugin.ParameterTypeString, "Path to a project.json file or a folder containing project.json file", true).
WithParameter("treat-warnings-as-errors", plugin.ParameterTypeBoolean, "Treat warnings as errors", false).
WithParameter("stop-on-rule-violation", plugin.ParameterTypeBoolean, "Fail when any rule is violated", false)
}

func (c PackageAnalyzeCommand) Execute(context plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
source, err := c.getSource(context)
if err != nil {
return err
}
treatWarningsAsErrors, _ := c.getBoolParameter("treat-warnings-as-errors", context.Parameters)
stopOnRuleViolation, _ := c.getBoolParameter("stop-on-rule-violation", context.Parameters)
exitCode, result, err := c.execute(source, treatWarningsAsErrors, stopOnRuleViolation, context.Debug, logger)
if err != nil {
return err
}

json, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("analyze command failed: %v", err)
}
err = writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
if err != nil {
return err
}
if exitCode != 0 {
return errors.New("")
}
return nil
}

func (c PackageAnalyzeCommand) execute(source string, treatWarningsAsErrors bool, stopOnRuleViolation bool, debug bool, logger log.Logger) (int, *packageAnalyzeResult, error) {
if !debug {
bar := c.newAnalyzingProgressBar(logger)
defer close(bar)
}

jsonResultFilePath, err := c.getTemporaryJsonResultFilePath()
if err != nil {
return 1, nil, err
}
defer os.Remove(jsonResultFilePath)

args := []string{"package", "analyze", source, "--resultPath", jsonResultFilePath}
if treatWarningsAsErrors {
args = append(args, "--treatWarningsAsErrors")
}
if stopOnRuleViolation {
args = append(args, "--stopOnRuleViolation")
}

uipcli := newUipcli(c.Exec, logger)
cmd, err := uipcli.Execute(args...)
if err != nil {
return 1, nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
}
defer stdout.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
}
defer stderr.Close()
err = cmd.Start()
if err != nil {
return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
}

stderrOutputBuilder := new(strings.Builder)
stderrReader := io.TeeReader(stderr, stderrOutputBuilder)

var wg sync.WaitGroup
wg.Add(3)
go c.readOutput(stdout, logger, &wg)
go c.readOutput(stderrReader, logger, &wg)
go c.wait(cmd, &wg)
wg.Wait()

violations, err := c.readAnalyzeResult(jsonResultFilePath)
if err != nil {
return 1, nil, err
}

exitCode := cmd.ExitCode()
var result *packageAnalyzeResult
if exitCode == 0 {
result = newSucceededPackageAnalyzeResult(violations)
} else {
result = newFailedPackageAnalyzeResult(
violations,
stderrOutputBuilder.String(),
)
}
return exitCode, result, nil
}

func (c PackageAnalyzeCommand) getTemporaryJsonResultFilePath() (string, error) {
tempDirectory, err := utils.Directories{}.Temp()
if err != nil {
return "", err
}
fileName := c.randomJsonResultFileName()
return filepath.Join(tempDirectory, fileName), nil
}

func (c PackageAnalyzeCommand) randomJsonResultFileName() string {
value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
return "analyzeresult-" + value.String() + ".json"
}

func (c PackageAnalyzeCommand) readAnalyzeResult(path string) ([]packageAnalyzeViolation, error) {
file, err := os.Open(path)
if err != nil {
return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
}
defer file.Close()
byteValue, err := io.ReadAll(file)
if err != nil {
return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
}

var result analyzeResultJson
err = json.Unmarshal(byteValue, &result)
if err != nil {
return []packageAnalyzeViolation{}, fmt.Errorf("Error parsing %s file: %v", filepath.Base(path), err)
}
return c.convertToViolations(result), nil
}

func (c PackageAnalyzeCommand) convertToViolations(json analyzeResultJson) []packageAnalyzeViolation {
violations := []packageAnalyzeViolation{}
for _, entry := range json {
var activityId *packageAnalyzeActivityId
if entry.ActivityId != nil {
activityId = &packageAnalyzeActivityId{
Id: entry.ActivityId.Id,
IdRef: entry.ActivityId.IdRef,
}
}
var item *packageAnalyzeItem
if entry.Item != nil {
item = &packageAnalyzeItem{
Name: entry.Item.Name,
Type: entry.Item.Type,
}
}
violation := packageAnalyzeViolation{
ErrorCode: entry.ErrorCode,
Description: entry.Description,
RuleName: entry.RuleName,
FilePath: entry.FilePath,
ActivityDisplayName: entry.ActivityDisplayName,
WorkflowDisplayName: entry.WorkflowDisplayName,
ErrorSeverity: entry.ErrorSeverity,
Recommendation: entry.Recommendation,
DocumentationLink: entry.DocumentationLink,
ActivityId: activityId,
Item: item,
}
violations = append(violations, violation)
}
return violations
}

func (c PackageAnalyzeCommand) wait(cmd utils.ExecCmd, wg *sync.WaitGroup) {
defer wg.Done()
_ = cmd.Wait()
}

func (c PackageAnalyzeCommand) newAnalyzingProgressBar(logger log.Logger) chan struct{} {
progressBar := utils.NewProgressBar(logger)
ticker := time.NewTicker(10 * time.Millisecond)
cancel := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
progressBar.Tick("analyzing... ")
case <-cancel:
ticker.Stop()
progressBar.Remove()
return
}
}
}()
return cancel
}

func (c PackageAnalyzeCommand) getSource(context plugin.ExecutionContext) (string, error) {
source, _ := c.getParameter("source", context.Parameters)
if source == "" {
return "", errors.New("source is not set")
}
fileInfo, err := os.Stat(source)
if err != nil {
return "", fmt.Errorf("%s not found", defaultProjectJson)
}
if fileInfo.IsDir() {
source = filepath.Join(source, defaultProjectJson)
}
return source, nil
}

func (c PackageAnalyzeCommand) readOutput(output io.Reader, logger log.Logger, wg *sync.WaitGroup) {
defer wg.Done()
scanner := bufio.NewScanner(output)
scanner.Split(bufio.ScanRunes)
for scanner.Scan() {
logger.Log(scanner.Text())
}
}

func (c PackageAnalyzeCommand) getParameter(name string, parameters []plugin.ExecutionParameter) (string, error) {
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(string); ok {
return data, nil
}
}
}
return "", fmt.Errorf("Could not find '%s' parameter", name)
}

func (c PackageAnalyzeCommand) getBoolParameter(name string, parameters []plugin.ExecutionParameter) (bool, error) {
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(bool); ok {
return data, nil
}
}
}
return false, fmt.Errorf("Could not find '%s' parameter", name)
}

func NewPackageAnalyzeCommand() *PackageAnalyzeCommand {
return &PackageAnalyzeCommand{utils.NewExecProcess()}
}
38 changes: 38 additions & 0 deletions plugin/studio/package_analyze_result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package studio

type packageAnalyzeResult struct {
Status string `json:"status"`
Violations []packageAnalyzeViolation `json:"violations"`
Error *string `json:"error"`
}

type packageAnalyzeViolation struct {
ErrorCode string `json:"errorCode"`
Description string `json:"description"`
RuleName string `json:"ruleName"`
FilePath string `json:"filePath"`
ActivityId *packageAnalyzeActivityId `json:"activityId"`
ActivityDisplayName string `json:"activityDisplayName"`
WorkflowDisplayName string `json:"workflowDisplayName"`
Item *packageAnalyzeItem `json:"item"`
ErrorSeverity int `json:"errorSeverity"`
Recommendation string `json:"recommendation"`
DocumentationLink string `json:"documentationLink"`
}
type packageAnalyzeActivityId struct {
Id string `json:"id"`
IdRef string `json:"idRef"`
}

type packageAnalyzeItem struct {
Name string `json:"name"`
Type int `json:"type"`
}

func newSucceededPackageAnalyzeResult(violations []packageAnalyzeViolation) *packageAnalyzeResult {
return &packageAnalyzeResult{"Succeeded", violations, nil}
}

func newFailedPackageAnalyzeResult(violations []packageAnalyzeViolation, err string) *packageAnalyzeResult {
return &packageAnalyzeResult{"Failed", violations, &err}
}
Loading

0 comments on commit cbd4e36

Please sign in to comment.