internal/platform/msg/output.go (233 lines of code) (raw):

/* * Copyright 2021-2024 JetBrains s.r.o. * * 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 * * https://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 msg import ( "fmt" "os" "strings" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" cienvironment "github.com/cucumber/ci-environment/go" "github.com/liamg/clinch/terminal" "github.com/mattn/go-isatty" "github.com/pterm/pterm" log "github.com/sirupsen/logrus" ) var QodanaInteractiveSelect = pterm.InteractiveSelectPrinter{ TextStyle: PrimaryStyle, DefaultText: "Please select the product to use", Options: []string{}, OptionStyle: PrimaryStyle, DefaultOption: "", MaxHeight: 5, Selector: ">", SelectorStyle: PrimaryStyle, } // InfoString Two newlines at the start are important to lay the output nicely in CLI. func InfoString(version string) string { return fmt.Sprintf( ` %s (%s) https://jb.gg/qodana-cli Documentation – https://jb.gg/qodana-docs Contact us at qodana-support@jetbrains.com Bug Tracker: https://jb.gg/qodana-issue Community forum: https://jb.gg/qodana-forum `, "Qodana CLI", version, ) } // IsInteractive returns true if the current execution environment is interactive (useful for colors/animations toggle). func IsInteractive() bool { return !qdenv.IsContainer() && os.Getenv("NONINTERACTIVE") == "" && (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())) } // DisableColor disables colors in the output. func DisableColor() { pterm.DisableColor() } // styles and different declarations intended to be used only inside this file var ( noLineWidth = 7 QodanaSpinner = pterm.DefaultSpinner spinnerSequence = []string{"| ", "/ ", "- ", "\\ "} PrimaryStyle = pterm.NewStyle() // PrimaryStyle is a primary text style. primaryBoldStyle = pterm.NewStyle(pterm.Bold) // primaryBoldStyle is a Primary bold text style. errorStyle = pterm.NewStyle(pterm.FgRed) // errorStyle is an error style. warningStyle = pterm.NewStyle(pterm.FgYellow) // warningStyle is a warning style. miscStyle = pterm.NewStyle(pterm.FgGray) // miscStyle is a log style. tableSep = miscStyle.Sprint("─") tableSepUp = miscStyle.Sprint("┬") tableSepMid = miscStyle.Sprint("│") tableSepDown = miscStyle.Sprint("┴") tableUp = strings.Repeat(tableSep, noLineWidth) + tableSepUp tableDown = strings.Repeat(tableSep, noLineWidth) + tableSepDown DefaultPromptText = "Do you want to continue?" ) // Primary prints a message in the Primary style. func Primary(text string, a ...interface{}) string { text = fmt.Sprintf(text, a...) return PrimaryStyle.Sprint(text) } // PrimaryBold prints a message in the primary bold style. func PrimaryBold(text string, a ...interface{}) string { text = fmt.Sprintf(text, a...) return primaryBoldStyle.Sprint(text) } // EmptyMessage is a message that is used when there is no message to show. func EmptyMessage() { pterm.Println() } // SuccessMessage prints a success message with the icon. func SuccessMessage(message string, a ...interface{}) { message = fmt.Sprintf(message, a...) icon := pterm.Green("✓ ") pterm.Println(icon, Primary(message)) } // WarningMessage prints a warning message with the icon. func WarningMessage(message string, a ...interface{}) { message = fmt.Sprintf(message, a...) icon := warningStyle.Sprint("\n! ") pterm.Println(icon, Primary(message)) } // WarningMessageCI prints a warning message to the CI environment (additional highlighting). func WarningMessageCI(message string, a ...interface{}) { message = fmt.Sprintf(message, a...) pterm.Println(formatMessageForCI("warning", "%s", message)) } // ErrorMessage prints an error message with the icon. func ErrorMessage(message string, a ...interface{}) { message = fmt.Sprintf(message, a...) icon := errorStyle.Sprint("✗ ") pterm.Println(icon, errorStyle.Sprint(message)) } // PrintLinterLog prints the linter logs with color, when needed. func PrintLinterLog(line string) { if strings.Contains(line, " / /") || strings.Contains(line, "_ _") || strings.Contains(line, "\\/__") || strings.Contains(line, "\\ \\") { PrimaryStyle.Println(line) } else { miscStyle.Println(line) } } // PrintProcess prints the message for processing phase. TODO: Add ETA based on previous runs func PrintProcess(f func(spinner *pterm.SpinnerPrinter), start string, finished string) { if err := spin(f, start); err != nil { log.Fatal("\nProblem occurred:", err.Error()) } if finished != "" { SuccessMessage("Finished %s", finished) } } // spin creates spinner and runs the given function. Also, spin is a spider in Dutch. func spin(fun func(spinner *pterm.SpinnerPrinter), message string) error { spinner, _ := StartQodanaSpinner(message) if spinner == nil { fmt.Println(Primary(message + "...")) } fun(spinner) if spinner != nil { spinner.Success() } return nil } // StartQodanaSpinner starts a new spinner with the given message. func StartQodanaSpinner(message string) (*pterm.SpinnerPrinter, error) { if IsInteractive() { QodanaSpinner.Sequence = spinnerSequence QodanaSpinner.MessageStyle = PrimaryStyle return QodanaSpinner.WithStyle(pterm.NewStyle(pterm.FgGray)).WithRemoveWhenDone(true).Start(message + "...") } return nil, nil } // UpdateText updates the text of the spinner. func UpdateText(spinner *pterm.SpinnerPrinter, message string) { if spinner != nil { spinner.UpdateText(message + "...") } } // PrintFile prints the given file content with lines like printProblem. func PrintFile(file string) { printHeader("", "", file) content, err := os.ReadFile(file) if err != nil { log.Fatalf("failed to read file %s: %s", file, err) } printLines(string(content), 1, 0, true) } // PrintProblem printProblem prints problem with source code or without it. func PrintProblem( ruleId string, level string, message string, path string, line int, column int, contextLine int, context string, ) { printHeader(level, ruleId, "") printPath(path, line, column) printLines(context, contextLine, line, false) fmt.Print(message + "\n") } // getTerminalWidth returns the width of the terminal. func getTerminalWidth() int { width, _ := terminal.Size() if width <= 0 { width = 80 } return width } // printHeader prints the header of the problem/file. func printHeader(level string, ruleId string, file string) { width := getTerminalWidth() fmt.Printf("%s %s\n", PrimaryBold(strings.ToUpper(level)), Primary(ruleId)) fmt.Println(strings.Repeat(tableSep, width)) if file != "" { fmt.Printf("%5s %s %s\n", "", tableSepMid, PrimaryBold(file)) fmt.Println(strings.Repeat(tableSep, width)) } } // printPath prints the path of the problem. func printPath(path string, line int, column int) { if path != "" && line > 0 && column > 0 { fmt.Printf(" %s:%d:%d\n", path, line, column) fmt.Printf("%s%s\n", tableUp, strings.Repeat(tableSep, getTerminalWidth()-noLineWidth-1)) } else { fmt.Println(strings.Repeat(tableSep, getTerminalWidth())) } } // printLines prints the lines of the problem. func printLines(content string, contextLine int, line int, skipHighlight bool) { lines := strings.Split(content, "\n") if lines[len(lines)-1] == "" { // Remove the last empty line if content ends with a newline lines = lines[:len(lines)-1] } for i := 0; i < len(lines); i++ { var printLine string currentLine := contextLine + i if skipHighlight { printLine = lines[i] } else if currentLine == line { printLine = errorStyle.Sprint(lines[i]) + " ←" } else { printLine = warningStyle.Sprint(lines[i]) } lineNumber := miscStyle.Sprintf("%5d", currentLine) fmt.Printf("%s %s %s\n", lineNumber, tableSepMid, printLine) } fmt.Printf("%s%s\n", tableDown, strings.Repeat(tableSep, getTerminalWidth()-noLineWidth-1)) } // GetProblemsFoundMessage returns a message about the number of problems found, used in CLI and BitBucket report. func GetProblemsFoundMessage(newProblems int) string { switch newProblems { case 0: return "It seems all right 👌 No new problems found according to the checks applied" case 1: return "Found 1 new problem according to the checks applied" default: return fmt.Sprintf("Found %d new problems according to the checks applied", newProblems) } } // formatMessageForCI formats the message for the CI environment. func formatMessageForCI(level, format string, a ...interface{}) string { message := fmt.Sprintf(format, a...) ci := cienvironment.DetectCIEnvironment() if ci != nil { name := qdenv.GetCIName(ci) if name == "github-actions" { return fmt.Sprintf("::%s::%s", level, message) } else if strings.HasPrefix(name, "azure") { return fmt.Sprintf("##vso[task.logissue type=%s]%s", level, message) } else if strings.HasPrefix(name, "circleci") { return fmt.Sprintf("echo '%s: %s'", level, message) } } return fmt.Sprintf("! %s", message) }