Skip to content

Commit 5de3c42

Browse files
authored
Feat/version hint (#51)
1 parent 8de40d2 commit 5de3c42

20 files changed

+870
-54
lines changed

Lexer.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ lexer grammar Lexer;
22
WS: [ \t\r\n]+ -> skip;
33
NEWLINE: [\r\n]+;
44
MULTILINE_COMMENT: '/*' (MULTILINE_COMMENT | .)*? '*/' -> skip;
5-
LINE_COMMENT: '//' .*? NEWLINE -> skip;
5+
LINE_COMMENT: '//' .*? NEWLINE -> channel(HIDDEN);
66

77
VARS: 'vars';
88
MAX: 'max';

internal/analysis/check.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type Diagnostic struct {
9191
}
9292

9393
type CheckResult struct {
94+
version parser.Version
9495
nextDiagnosticId int32
9596
unboundedAccountInSend parser.ValueExpr
9697
emptiedAccount map[string]struct{}
@@ -141,6 +142,7 @@ func (r CheckResult) ResolveBuiltinFn(v *parser.FnCallIdentifier) FnCallResoluti
141142

142143
func newCheckResult(program parser.Program) CheckResult {
143144
return CheckResult{
145+
version: program.GetVersion(),
144146
emptiedAccount: make(map[string]struct{}),
145147
declaredVars: make(map[string]parser.VarDeclaration),
146148
unusedVars: make(map[string]parser.Range),
@@ -303,6 +305,8 @@ func (res *CheckResult) checkDuplicateVars(variableName parser.Variable, decl pa
303305
}
304306

305307
func (res *CheckResult) checkFnCall(fnCall parser.FnCall) string {
308+
res.checkOvedraftFunctionVersion(fnCall)
309+
306310
returnType := TypeAny
307311

308312
if resolution, ok := Builtins[fnCall.Caller.Name]; ok {
@@ -353,6 +357,8 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr, typeHint string) strin
353357
return res.checkInfixOverload(lit, []string{TypeNumber, TypeMonetary})
354358

355359
case parser.InfixOperatorDiv:
360+
res.checkInfixVersion(*lit)
361+
356362
res.checkExpression(lit.Left, TypeNumber)
357363
res.checkExpression(lit.Right, TypeNumber)
358364
return TypePortion
@@ -366,6 +372,8 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr, typeHint string) strin
366372
}
367373

368374
case *parser.AccountInterpLiteral:
375+
res.checkAccountInterpolationVersion(*lit)
376+
369377
for _, part := range lit.Parts {
370378
if v, ok := part.(*parser.Variable); ok {
371379
res.checkExpression(v, TypeAny)
@@ -477,6 +485,8 @@ func (res *CheckResult) checkSource(source parser.Source) {
477485
}
478486

479487
case *parser.SourceOneof:
488+
res.checkOneofVersion(source.Range)
489+
480490
for _, source := range source.Sources {
481491
res.checkSource(source)
482492
}
@@ -631,6 +641,8 @@ func (res *CheckResult) checkDestination(destination parser.Destination) {
631641
res.checkKeptOrDestination(destination.Remaining)
632642

633643
case *parser.DestinationOneof:
644+
res.checkOneofVersion(destination.Range)
645+
634646
for _, clause := range destination.Clauses {
635647
res.checkExpression(clause.Cap, TypeMonetary)
636648
res.checkKeptOrDestination(clause.To)

internal/analysis/check_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/formancehq/numscript/internal/analysis"
7+
"github.com/formancehq/numscript/internal/flags"
78
"github.com/formancehq/numscript/internal/parser"
89

910
"github.com/stretchr/testify/assert"
@@ -671,3 +672,192 @@ send [EUR/2 *] (
671672
d1.Range,
672673
)
673674
}
675+
676+
func TestDoNoCheckVersionWhenNotSpecified(t *testing.T) {
677+
t.Parallel()
678+
679+
input := `
680+
vars {
681+
number $n
682+
}
683+
684+
send [EUR/2 100] (
685+
source = {
686+
1/$n from @a
687+
remaining from @b
688+
}
689+
destination = @dest
690+
)
691+
`
692+
693+
require.Equal(t,
694+
[]analysis.Diagnostic(nil),
695+
checkSource(input),
696+
)
697+
698+
}
699+
700+
func TestRequireVersionForInfixDiv(t *testing.T) {
701+
t.Parallel()
702+
703+
input := `
704+
// @version machine
705+
706+
vars {
707+
number $n
708+
}
709+
710+
send [EUR/2 100] (
711+
source = {
712+
1/$n from @a
713+
remaining from @b
714+
}
715+
destination = @dest
716+
)
717+
`
718+
719+
require.Equal(t,
720+
[]analysis.Diagnostic{
721+
{
722+
Range: parser.RangeOfIndexed(input, "1/$n", 0),
723+
Kind: analysis.VersionMismatch{
724+
GotVersion: parser.VersionMachine{},
725+
RequiredVersion: parser.NewVersionInterpreter(0, 0, 15),
726+
},
727+
},
728+
},
729+
checkSource(input),
730+
)
731+
}
732+
733+
func TestRequireVersionForInfixDivWhenVersionLt(t *testing.T) {
734+
t.Parallel()
735+
736+
input := `
737+
// required version is 0.0.15
738+
// @version interpreter 0.0.1
739+
740+
vars {
741+
number $n
742+
}
743+
744+
send [EUR/2 100] (
745+
source = {
746+
1/$n from @a
747+
remaining from @b
748+
}
749+
destination = @dest
750+
)
751+
`
752+
753+
require.Equal(t,
754+
[]analysis.Diagnostic{
755+
{
756+
Range: parser.RangeOfIndexed(input, "1/$n", 0),
757+
Kind: analysis.VersionMismatch{
758+
GotVersion: parser.NewVersionInterpreter(0, 0, 1),
759+
RequiredVersion: parser.NewVersionInterpreter(0, 0, 15),
760+
},
761+
},
762+
},
763+
checkSource(input),
764+
)
765+
}
766+
767+
func TestRequireFlagForOneofWhenMissing(t *testing.T) {
768+
t.Parallel()
769+
770+
input := `
771+
// @version interpreter 0.0.15
772+
773+
send [EUR/2 100] (
774+
source = oneof {
775+
@a
776+
@b
777+
}
778+
destination = @dest
779+
)
780+
`
781+
782+
ds := checkSource(input)
783+
require.Len(t, ds, 1)
784+
785+
require.Equal(t,
786+
analysis.ExperimentalFeature{
787+
Name: flags.ExperimentalOneofFeatureFlag,
788+
},
789+
ds[0].Kind,
790+
)
791+
}
792+
793+
func TestRequireFlagForOneofWhenGiven(t *testing.T) {
794+
t.Parallel()
795+
796+
input := `
797+
// @version interpreter 0.0.15
798+
// @feature_flag experimental-oneof
799+
800+
send [EUR/2 100] (
801+
source = oneof {
802+
@a
803+
@b
804+
}
805+
destination = @dest
806+
)
807+
`
808+
809+
require.Empty(t, checkSource(input))
810+
}
811+
812+
func TestRequireFlagForInterpolation(t *testing.T) {
813+
t.Parallel()
814+
815+
input := `
816+
// @version interpreter 0.0.15
817+
vars {
818+
number $id
819+
}
820+
821+
send [EUR/2 100] (
822+
source = @user:$id
823+
destination = @dest
824+
)
825+
`
826+
827+
require.Equal(t,
828+
[]analysis.Diagnostic{
829+
{
830+
Range: parser.RangeOfIndexed(input, "@user:$id", 0),
831+
Kind: analysis.ExperimentalFeature{
832+
Name: flags.ExperimentalAccountInterpolationFlag,
833+
},
834+
},
835+
},
836+
checkSource(input),
837+
)
838+
}
839+
840+
func TestRequireFlagForFunctionOverdraft(t *testing.T) {
841+
t.Parallel()
842+
843+
input := `
844+
// @version interpreter 0.0.15
845+
vars {
846+
monetary $m = overdraft(@acc, USD/2)
847+
}
848+
849+
send $m ( source = @world destination = @dest)
850+
`
851+
852+
require.Equal(t,
853+
[]analysis.Diagnostic{
854+
{
855+
Range: parser.RangeOfIndexed(input, "overdraft(@acc, USD/2)", 0),
856+
Kind: analysis.ExperimentalFeature{
857+
Name: flags.ExperimentalOverdraftFunctionFeatureFlag,
858+
},
859+
},
860+
},
861+
checkSource(input),
862+
)
863+
}

internal/analysis/diagnostic_kind.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math/big"
66

77
"github.com/formancehq/numscript/internal/ansi"
8+
"github.com/formancehq/numscript/internal/parser"
89
"github.com/formancehq/numscript/internal/utils"
910
)
1011

@@ -271,3 +272,28 @@ func (e UnboundedAccountIsNotLast) Message() string {
271272
func (UnboundedAccountIsNotLast) Severity() Severity {
272273
return WarningSeverity
273274
}
275+
276+
type VersionMismatch struct {
277+
RequiredVersion parser.Version
278+
GotVersion parser.Version
279+
}
280+
281+
func (e VersionMismatch) Message() string {
282+
return fmt.Sprintf("Version mismatch. Required version '%s' (but got '%s' instead)", e.RequiredVersion.String(), e.GotVersion.String())
283+
}
284+
285+
func (VersionMismatch) Severity() Severity {
286+
return ErrorSeverity
287+
}
288+
289+
type ExperimentalFeature struct {
290+
Name string
291+
}
292+
293+
func (e ExperimentalFeature) Message() string {
294+
return fmt.Sprintf("This feature is experimental. Add this comment to your script:\n// @feature_flag %s", e.Name)
295+
}
296+
297+
func (ExperimentalFeature) Severity() Severity {
298+
return ErrorSeverity
299+
}

0 commit comments

Comments
 (0)