-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support to analyze studio projects
- Loading branch information
Showing
10 changed files
with
507 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
Oops, something went wrong.