mirror of
https://github.com/hanzoai/numscript.git
synced 2026-10-30 18:26:57 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers, and the interpreter's parseArg, evaluateExprAs and evaluateOptExprAs are now generic methods on the parser and program state they take. omitzero is not applied, since it changes what the JSON says. Co-Authored-By: Claude Opus 5 <[email protected]>
372 lines
8.1 KiB
Go
372 lines
8.1 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/formancehq/numscript/internal/ansi"
|
|
"github.com/formancehq/numscript/internal/parser"
|
|
"github.com/formancehq/numscript/internal/utils"
|
|
)
|
|
|
|
type Severity = byte
|
|
|
|
// !important! keep in sync with LSP specs
|
|
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity
|
|
const (
|
|
_ Severity = iota
|
|
ErrorSeverity
|
|
WarningSeverity
|
|
Information
|
|
Hint
|
|
)
|
|
|
|
func SeverityToAnsiString(s Severity) string {
|
|
switch s {
|
|
case ErrorSeverity:
|
|
return ansi.ColorRed("Error")
|
|
case WarningSeverity:
|
|
return ansi.ColorYellow("Warning")
|
|
case Information:
|
|
return "Info"
|
|
case Hint:
|
|
return "Hint"
|
|
default:
|
|
return utils.NonExhaustiveMatchPanic[string](s)
|
|
}
|
|
}
|
|
|
|
func SeverityToString(s Severity) string {
|
|
switch s {
|
|
case ErrorSeverity:
|
|
return "Error"
|
|
case WarningSeverity:
|
|
return "Warning"
|
|
case Information:
|
|
return "Info"
|
|
case Hint:
|
|
return "Hint"
|
|
default:
|
|
return utils.NonExhaustiveMatchPanic[string](s)
|
|
}
|
|
}
|
|
|
|
type DiagnosticKind interface {
|
|
Message() string
|
|
Severity() Severity
|
|
}
|
|
|
|
// ###### Diagnostics
|
|
|
|
type Parsing struct {
|
|
Description string
|
|
}
|
|
|
|
func (e Parsing) Message() string {
|
|
return e.Description
|
|
}
|
|
|
|
func (Parsing) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type InvalidType struct {
|
|
Name string
|
|
}
|
|
|
|
// TODO evaluate suggestion using Levenshtein distance
|
|
func (e InvalidType) Message() string {
|
|
var allowedTypeList strings.Builder
|
|
for index, t := range AllowedTypes {
|
|
if index != 0 {
|
|
allowedTypeList.WriteString(", ")
|
|
}
|
|
allowedTypeList.WriteString(t)
|
|
}
|
|
|
|
return fmt.Sprintf("'%s' is not a valid type. Allowed types are: %s", e.Name, allowedTypeList.String())
|
|
}
|
|
|
|
func (InvalidType) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type DuplicateVariable struct {
|
|
Name string
|
|
}
|
|
|
|
func (e DuplicateVariable) Message() string {
|
|
return fmt.Sprintf("A variable with the name '$%s' was already declared", e.Name)
|
|
}
|
|
|
|
func (DuplicateVariable) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type UnboundVariable struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
|
|
// TODO evaluate suggestion using Levenshtein distance
|
|
func (e UnboundVariable) Message() string {
|
|
return fmt.Sprintf("The variable '$%s' was not declared", e.Name)
|
|
}
|
|
|
|
func (UnboundVariable) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type UnusedVar struct {
|
|
Name string
|
|
}
|
|
|
|
func (e UnusedVar) Message() string {
|
|
return fmt.Sprintf("The variable '$%s' is never used", e.Name)
|
|
}
|
|
|
|
func (UnusedVar) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type TypeMismatch struct {
|
|
Expected string
|
|
Got string
|
|
}
|
|
|
|
func (e TypeMismatch) Message() string {
|
|
return fmt.Sprintf("Type mismatch (expected '%s', got '%s' instead)", e.Expected, e.Got)
|
|
}
|
|
|
|
func (TypeMismatch) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type AssetMismatch struct {
|
|
Expected string
|
|
Got string
|
|
}
|
|
|
|
func (e AssetMismatch) Message() string {
|
|
return fmt.Sprintf("Asset mismatch (expected '%s', got '%s' instead)", e.Expected, e.Got)
|
|
}
|
|
|
|
func (AssetMismatch) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type RemainingIsNotLast struct{}
|
|
|
|
func (e RemainingIsNotLast) Message() string {
|
|
return "A 'remaining' clause should be the last in an allotment expression"
|
|
}
|
|
func (RemainingIsNotLast) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type BadAllotmentSum struct {
|
|
Sum big.Rat
|
|
}
|
|
|
|
func (e BadAllotmentSum) Message() string {
|
|
one := big.NewRat(1, 1)
|
|
|
|
switch e.Sum.Cmp(one) {
|
|
// sum > 1
|
|
case 1:
|
|
return fmt.Sprintf("Allotment portions are greater than one (Got %s)", e.Sum.String())
|
|
|
|
// sum < 1
|
|
case -1:
|
|
return fmt.Sprintf("Allotment portions are lesser than one (Got %s). Maybe try adding a 'remaining' clause?", e.Sum.String())
|
|
}
|
|
|
|
panic(fmt.Sprintf("unreachable state: allotment=%s", e.Sum.String()))
|
|
}
|
|
func (BadAllotmentSum) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type DivByZero struct {
|
|
Sum big.Rat
|
|
}
|
|
|
|
func (e DivByZero) Message() string {
|
|
return "Cannot divide by zero"
|
|
}
|
|
|
|
func (DivByZero) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type FixedPortionVariable struct {
|
|
Value big.Rat
|
|
}
|
|
|
|
func (e FixedPortionVariable) Message() string {
|
|
return fmt.Sprintf("Using a variable expression can lead to a runtime error if the expression doesn't resolve to %s.\n\nConsider using a hard-coded value or adding a 'remaining' clause to prevent the error", e.Value.String())
|
|
}
|
|
func (FixedPortionVariable) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type RedundantRemaining struct{}
|
|
|
|
func (e RedundantRemaining) Message() string {
|
|
return "Redundant 'remaining' clause (allotment already sums to 1)"
|
|
}
|
|
func (RedundantRemaining) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type UnknownFunction struct {
|
|
Name string
|
|
}
|
|
|
|
func (e UnknownFunction) Message() string {
|
|
res, exists := Builtins[e.Name]
|
|
if exists {
|
|
return fmt.Sprintf("You cannot use this function here (try to use it in a %s context)", res.ContextName())
|
|
}
|
|
// TODO suggest alternatives using Levenshtein distance
|
|
return fmt.Sprintf("The function '%s' does not exist", e.Name)
|
|
}
|
|
|
|
func (UnknownFunction) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type BadArity struct {
|
|
Expected int
|
|
Actual int
|
|
}
|
|
|
|
func (e BadArity) Message() string {
|
|
return fmt.Sprintf("Wrong number of arguments (expected %d, got %d instead)", e.Expected, e.Actual)
|
|
}
|
|
|
|
func (BadArity) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type InvalidWorldOverdraft struct{}
|
|
|
|
func (e InvalidWorldOverdraft) Message() string {
|
|
return "@world is already set to be ovedraft"
|
|
}
|
|
|
|
func (InvalidWorldOverdraft) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type NoAllotmentInSendAll struct{}
|
|
|
|
func (e NoAllotmentInSendAll) Message() string {
|
|
return "Cannot take all balance of an allotment source"
|
|
}
|
|
|
|
func (NoAllotmentInSendAll) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type InvalidUnboundedAccount struct{}
|
|
|
|
func (e InvalidUnboundedAccount) Message() string {
|
|
return "Cannot take all balance of an unbounded source"
|
|
}
|
|
|
|
func (InvalidUnboundedAccount) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type EmptiedAccount struct {
|
|
Name string
|
|
}
|
|
|
|
func (e EmptiedAccount) Message() string {
|
|
return fmt.Sprintf("@%s is already empty at this point", e.Name)
|
|
}
|
|
|
|
func (EmptiedAccount) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type UnboundedAccountIsNotLast struct{}
|
|
|
|
func (e UnboundedAccountIsNotLast) Message() string {
|
|
return "Inorder sources after an unbounded overdraft are never reached"
|
|
}
|
|
|
|
func (UnboundedAccountIsNotLast) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type VersionMismatch struct {
|
|
RequiredVersion parser.Version
|
|
GotVersion parser.Version
|
|
}
|
|
|
|
func (e VersionMismatch) Message() string {
|
|
return fmt.Sprintf("Version mismatch. Required version '%s' (but got '%s' instead)", e.RequiredVersion.String(), e.GotVersion.String())
|
|
}
|
|
|
|
func (VersionMismatch) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type ExperimentalFeature struct {
|
|
Name string
|
|
}
|
|
|
|
func (e ExperimentalFeature) Message() string {
|
|
return fmt.Sprintf("This feature is experimental. Add this comment to your script:\n// @feature_flag %s", e.Name)
|
|
}
|
|
|
|
func (ExperimentalFeature) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|
|
|
|
type BoundAssetType = uint
|
|
|
|
const (
|
|
BoundAssetTypeMonetary BoundAssetType = iota
|
|
BoundAssetTypeAsset
|
|
)
|
|
|
|
type BoundAsset struct {
|
|
BoundAssetType BoundAssetType
|
|
InferredAsset string
|
|
}
|
|
|
|
func (e BoundAsset) Message() string {
|
|
switch e.BoundAssetType {
|
|
case BoundAssetTypeAsset:
|
|
return fmt.Sprintf(`This asset is inferred to be '%s'. Receiving a different asset will cause a runtime error.
|
|
You may want to remove this variable and use the hardcoded value instead.`, e.InferredAsset)
|
|
|
|
case BoundAssetTypeMonetary:
|
|
return fmt.Sprintf(`This monetary is inferred to always have asset '%s'. Receiving a monetary of different asset will cause a runtime error.
|
|
You may want to use a variable of type number instead.`, e.InferredAsset)
|
|
|
|
default:
|
|
return utils.NonExhaustiveMatchPanic[string](e)
|
|
}
|
|
}
|
|
|
|
func (BoundAsset) Severity() Severity {
|
|
return WarningSeverity
|
|
}
|
|
|
|
type InvalidFeature struct {
|
|
Feature string
|
|
}
|
|
|
|
func (e InvalidFeature) Message() string {
|
|
return fmt.Sprintf("Unknown feature: %s", e.Feature)
|
|
}
|
|
|
|
func (InvalidFeature) Severity() Severity {
|
|
return ErrorSeverity
|
|
}
|