From 0bc3df0d59b6efedd1a5e9879dcbdfa177cf1793 Mon Sep 17 00:00:00 2001 From: Matthias Ladkau Date: Sat, 9 Jan 2021 12:07:22 +0000 Subject: [PATCH] feat: Merging 1.4.0 release --- .gitignore | 5 +- CHANGELOG.md | 42 +++++ README.md | 31 +++- cli/tool/debug.go | 29 ++-- cli/tool/format.go | 56 +++--- cli/tool/interpret.go | 44 ++++- cli/tool/interpret_test.go | 7 +- config/config.go | 2 +- parser/lexer_test.go | 4 +- parser/main_test.go | 4 +- parser/parser_main_test.go | 4 +- parser/prettyprinter.go | 218 ++++++++++++++++++------ parser/prettyprinter_test.go | 322 ++++++++++++++++++++++++++++++++++- scope/helper.go | 58 +++++++ scope/helper_test.go | 29 ++++ 15 files changed, 741 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index 763f420..a61cf55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -ecal +/ecal +/ecal.exe /.cache /.cover /.ecal_console_history @@ -7,6 +8,8 @@ ecal /coverage.html /dist /build +/examples/embedding/embedding +/examples/plugin/*.so /ecal-support/node_modules /ecal-support/out /ecal-support/*.vsix diff --git a/CHANGELOG.md b/CHANGELOG.md index 87eb60b..92b1497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.4.0](https://devt.de///compare/v1.3.3...v1.4.0) (2021-01-09) + + +### Features + +* Better pretty printer ([18e42f7](https://devt.de///commit/18e42f771dc386a8a6cfb9329f59c5a246934be0)) + +### [1.3.3](https://devt.de///compare/v1.3.2...v1.3.3) (2021-01-01) + + +### Bug Fixes + +* Not stopping debug server if not running in interactive mode ([4d62c35](https://devt.de///commit/4d62c353384d5cc718b891540abf0e5e344d9326)) + +### [1.3.2](https://devt.de///compare/v1.3.1...v1.3.2) (2020-12-30) + + +### Bug Fixes + +* Adding error display when reloading interpreter state ([f5cc392](https://devt.de///commit/f5cc392a9cd4dd2a1d743fd9f1bc54bb71f1484a)) + +### [1.3.1](https://devt.de///compare/v1.3.0...v1.3.1) (2020-12-30) + + +### Bug Fixes + +* Stopping and starting the processor when loading the initial file. ([981956f](https://devt.de///commit/981956ff9309b8395081d25a2c5711d872818015)) + +## [1.3.0](https://devt.de///compare/v1.2.0...v1.3.0) (2020-12-29) + + +### Features + +* Adding conversion helper for JSON objects ([1050423](https://devt.de///commit/1050423c453169f22da029a52f131e2d3054a1f1)) + +## [1.2.0](https://devt.de///compare/v1.1.0...v1.2.0) (2020-12-26) + + +### Features + +* Adding conversion helper for JSON objects ([0454de3](https://devt.de///commit/0454de30f32b70cb936675c86bf6bdafd4e4bc3b)) + ## [1.1.0](https://devt.de///compare/v1.0.4...v1.1.0) (2020-12-13) diff --git a/README.md b/README.md index 86138cb..f33177f 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,10 @@ ECAL ECAL is an ECA (Event Condition Action) language for concurrent event processing. ECAL can define event-based systems using rules which are triggered by events. ECAL is intended to be embedded into other software to provide an easy to use scripting language which can react to external events. -

-Code coverage - -Go Report Card - -Go Doc -

+[![Code coverage](https://void.devt.de/pub/ecal/test_result.svg)](https://void.devt.de/pub/ecal/coverage.txt) +[![Go Report Card](https://goreportcard.com/badge/devt.de/krotik/ecal?style=flat-square)](https://goreportcard.com/report/devt.de/krotik/ecal) +[![Go Reference](https://pkg.go.dev/badge/krotik/ecal.svg)](https://pkg.go.dev/devt.de/krotik/ecal) +[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) Features -------- @@ -83,7 +80,7 @@ The interpreter can be run in debug mode which adds debug commands to the consol It is possible to package your ECAL project into an executable that can be run without a separate ECAL interpreter. Run the `sh pack.sh` and see the script for details. -### Embedding ECAL +### Embedding ECAL and using event processing The primary purpose of ECAL is to be a simple multi-purpose language which can be embedded into other software: - It has a minimal (quite generic) syntax. @@ -115,9 +112,11 @@ If events are to be used then the processor of the runtime provider needs to be ``` rtp.Processor.Start() ``` +The processor must be started *after* all sinks have been declared and *before* events are thrown. + Events can then be injected into the interpreter. ``` -monitor, err := rtp.Processor.AddEventAndWait(engine.NewEvent("MyEvent", []string{"foo", "bar"}, map[interface{}]interface{}{ +monitor, err := rtp.Processor.AddEventAndWait(engine.NewEvent("MyEvent", []string{"foo", "bar", "myevent"}, map[interface{}]interface{}{ "data1": 123, "data2": "123", }), nil) @@ -126,6 +125,20 @@ All errors are collected in the returned monitor. ``` monitor.RootMonitor().AllErrors() ``` +The above event could be handled in ECAL with the following sinks: +``` +sink mysink + kindmatch [ "foo.bar.myevent" ], +{ + log("Got event: ", event) +} + +sink mysink2 + kindmatch [ "foo.*.*" ], +{ + log("Got event: ", event) +} +``` ### Using Go plugins in ECAL diff --git a/cli/tool/debug.go b/cli/tool/debug.go index a7e5bbb..dcf1fff 100644 --- a/cli/tool/debug.go +++ b/cli/tool/debug.go @@ -45,16 +45,16 @@ type CLIDebugInterpreter struct { BreakOnStart *bool // Flag if the debugger should stop the execution on start BreakOnError *bool // Flag if the debugger should stop when encountering an error - // Log output + LogOut io.Writer // Log output - LogOut io.Writer + debugServer *debugTelnetServer // Debug server if started } /* NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities. */ func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter { - return &CLIDebugInterpreter{i, nil, nil, nil, nil, nil, nil, os.Stdout} + return &CLIDebugInterpreter{i, nil, nil, nil, nil, nil, nil, os.Stdout, nil} } /* @@ -112,20 +112,17 @@ func (i *CLIDebugInterpreter) Interpret() error { // Start the debug server - debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ", + i.debugServer = &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ", nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger} wg := &sync.WaitGroup{} wg.Add(1) - go debugServer.Run(wg) + go i.debugServer.Run(wg) wg.Wait() - defer func() { - if debugServer.listener != nil { - debugServer.listen = false - debugServer.listener.Close() // Attempt to cleanup - } - }() + if *i.Interactive { + defer i.StopDebugServer() + } } err = i.CLIInterpreter.Interpret(*i.Interactive) @@ -134,6 +131,16 @@ func (i *CLIDebugInterpreter) Interpret() error { return err } +/* +StopDebugServer stops the debug server if it was started. +*/ +func (i *CLIDebugInterpreter) StopDebugServer() { + if i.debugServer != nil && i.debugServer.listener != nil { + i.debugServer.listen = false + i.debugServer.listener.Close() // Attempt to cleanup + } +} + /* LoadInitialFile clears the global scope and reloads the initial file. */ diff --git a/cli/tool/format.go b/cli/tool/format.go index 8b8530b..11a2ec5 100644 --- a/cli/tool/format.go +++ b/cli/tool/format.go @@ -25,8 +25,6 @@ import ( Format formats a given set of ECAL files. */ func Format() error { - var err error - wd, _ := os.Getwd() dir := flag.String("dir", wd, "Root directory for ECAL files") @@ -54,31 +52,49 @@ func Format() error { fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Formatting all %v files in %v", *ext, *dir)) - err = filepath.Walk(*dir, - func(path string, i os.FileInfo, err error) error { - if err == nil && !i.IsDir() { - var data []byte - var ast *parser.ASTNode - var srcFormatted string + return FormatFiles(*dir, *ext) +} + +/* +FormatFiles formats all ECAL files in a given directory with a given ending. +*/ +func FormatFiles(dir string, ext string) error { + var err error + + // Try to resolve symbolic links + + scanDir, lerr := os.Readlink(dir) + if lerr != nil { + scanDir = dir + } + + if err == nil { + err = filepath.Walk(scanDir, + func(path string, i os.FileInfo, err error) error { + if err == nil && !i.IsDir() { + var data []byte + var ast *parser.ASTNode + var srcFormatted string - if strings.HasSuffix(path, *ext) { - if data, err = ioutil.ReadFile(path); err == nil { - var ferr error + if strings.HasSuffix(path, ext) { + if data, err = ioutil.ReadFile(path); err == nil { + var ferr error - if ast, ferr = parser.Parse(path, string(data)); ferr == nil { - if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil { - ioutil.WriteFile(path, []byte(srcFormatted), i.Mode()) + if ast, ferr = parser.Parse(path, string(data)); ferr == nil { + if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil { + ioutil.WriteFile(path, []byte(fmt.Sprintln(srcFormatted)), i.Mode()) + } } - } - if ferr != nil { - fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Could not format %v: %v", path, ferr)) + if ferr != nil { + fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Could not format %v: %v", path, ferr)) + } } } } - } - return err - }) + return err + }) + } return err } diff --git a/cli/tool/interpret.go b/cli/tool/interpret.go index 33805a9..7dd6c07 100644 --- a/cli/tool/interpret.go +++ b/cli/tool/interpret.go @@ -177,6 +177,9 @@ LoadInitialFile clears the global scope and reloads the initial file. func (i *CLIInterpreter) LoadInitialFile(tid uint64) error { var err error + i.RuntimeProvider.Processor.Finish() + i.RuntimeProvider.Processor.Reset() + if i.CustomHandler != nil { i.CustomHandler.LoadInitialFile(tid) } @@ -203,6 +206,8 @@ func (i *CLIInterpreter) LoadInitialFile(tid uint64) error { } } + i.RuntimeProvider.Processor.Start() + return err } @@ -363,22 +368,18 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64) ot.WriteString(fmt.Sprint("\n")) ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n")) ot.WriteString(fmt.Sprint("\n")) + ot.WriteString(fmt.Sprint(" @format - Format all .ecal files in the current root directory.\n")) ot.WriteString(fmt.Sprint(" @reload - Clear the interpreter and reload the initial file if it was given.\n")) - ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n")) ot.WriteString(fmt.Sprint(" @std [glob] - List all available constants and functions of a stdlib package.\n")) + ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n")) if i.CustomHelpString != "" { ot.WriteString(i.CustomHelpString) } ot.WriteString(fmt.Sprint("\n")) ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n")) - } else if strings.HasPrefix(line, "@reload") { - - // Reload happens in a separate thread as it may be suspended on start - - go i.LoadInitialFile(i.RuntimeProvider.NewThreadID()) - ot.WriteString(fmt.Sprintln(fmt.Sprintln("Reloading interpreter state"))) - + } else if i.handleSpecialStatements(ot, line) { + return } else if strings.HasPrefix(line, "@sym") { i.displaySymbols(ot, strings.Split(line, " ")[1:]) @@ -416,6 +417,33 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64) } } +/* +handleSpecialStatements handles inbuild special statements. +*/ +func (i *CLIInterpreter) handleSpecialStatements(ot OutputTerminal, line string) bool { + + if strings.HasPrefix(line, "@format") { + err := FormatFiles(*i.Dir, ".ecal") + ot.WriteString(fmt.Sprintln(fmt.Sprintln("Files formatted:", err))) + + return true + + } else if strings.HasPrefix(line, "@reload") { + + // Reload happens in a separate thread as it may be suspended on start + + go func() { + err := i.LoadInitialFile(i.RuntimeProvider.NewThreadID()) + ot.WriteString(fmt.Sprintln(fmt.Sprintln("Interpreter reloaded:", err))) + }() + ot.WriteString(fmt.Sprintln(fmt.Sprintln("Reloading interpreter state"))) + + return true + } + + return false +} + /* displaySymbols lists all available inbuild functions and available stdlib packages of ECAL. */ diff --git a/cli/tool/interpret_test.go b/cli/tool/interpret_test.go index fefe1f5..4356518 100644 --- a/cli/tool/interpret_test.go +++ b/cli/tool/interpret_test.go @@ -323,7 +323,7 @@ func TestHandleInput(t *testing.T) { l2 := "" tin.LogLevel = &l2 - testTerm.in = []string{"?", "@reload", "@sym", "@std", "@cus", "q"} + testTerm.in = []string{"?", "@format", "@reload", "@sym", "@std", "@cus", "q"} if err := tin.Interpret(true); err != nil { t.Error("Unexpected result:", err) @@ -346,7 +346,7 @@ func TestHandleInput(t *testing.T) { return } - if testTerm.out.String() != `╒═════════════════╤═══════════════════════════════╕ + if strings.HasSuffix(testTerm.out.String(), `╒═════════════════╤═══════════════════════════════╕ │Inbuild function │Description │ ╞═════════════════╪═══════════════════════════════╡ │raise │Raise returns an error object. │ @@ -364,7 +364,8 @@ func TestHandleInput(t *testing.T) { │foo.Println │xxx │ │ │ │ ╘════════════╧════════════╛ -` { + +`) { t.Error("Unexpected result:", testTerm.out.String()) return } diff --git a/config/config.go b/config/config.go index 65e6f1f..16c3163 100644 --- a/config/config.go +++ b/config/config.go @@ -23,7 +23,7 @@ import ( /* ProductVersion is the current version of ECAL */ -const ProductVersion = "1.1.0" +const ProductVersion = "1.4.0" /* Known configuration options for ECAL diff --git a/parser/lexer_test.go b/parser/lexer_test.go index 5e4bb64..b007801 100644 --- a/parser/lexer_test.go +++ b/parser/lexer_test.go @@ -271,10 +271,10 @@ func TestCommentLexing(t *testing.T) { input := `name /* foo bar - x*/ 'b/* - */la' /*test*/` + x*/ 'b/* - */la' /*test*/` if res := LexToList("mytest", input); fmt.Sprint(res) != `["name" /* foo bar - x */ v:"b/* - */la" /* test */ EOF]` { + x */ v:"b/* - */la" /* test */ EOF]` { t.Error("Unexpected lexer result:", res) return } diff --git a/parser/main_test.go b/parser/main_test.go index 667e5ff..25aecc4 100644 --- a/parser/main_test.go +++ b/parser/main_test.go @@ -147,7 +147,7 @@ func markASTNodesAsPrettyPrinted(n *ASTNode) { func UnitTestPrettyPrinting(input, astOutput, ppOutput string) error { astres, err := ParseWithRuntime("mytest", input, &DummyRuntimeProvider{}) - if err != nil || fmt.Sprint(astres) != astOutput { + if err != nil || (astOutput != "" && fmt.Sprint(astres) != astOutput) { return fmt.Errorf("Unexpected parser output:\n%v expected was:\n%v Error: %v", astres, astOutput, err) } @@ -161,7 +161,7 @@ func UnitTestPrettyPrinting(input, astOutput, ppOutput string) error { // Make sure the pretty printed result is valid and gets the same parse tree astres2, err := ParseWithRuntime("mytest", ppres, &DummyRuntimeProvider{}) - if err != nil || fmt.Sprint(astres2) != astOutput { + if err != nil || (astOutput != "" && fmt.Sprint(astres2) != astOutput) { return fmt.Errorf("Unexpected parser output from pretty print string:\n%v expected was:\n%v Error: %v", astres2, astOutput, err) } diff --git a/parser/parser_main_test.go b/parser/parser_main_test.go index 6b2c562..ae7275f 100644 --- a/parser/parser_main_test.go +++ b/parser/parser_main_test.go @@ -102,10 +102,10 @@ func TestCommentParsing(t *testing.T) { // Comment parsing without statements - input := `/* This is a comment*/ a := 1 + 1 # foo bar` + input := `/* This is a comment */ a := 1 + 1 # foo bar` expectedOutput := ` := - identifier: a # This is a comment + identifier: a # This is a comment plus number: 1 number: 1 # foo bar diff --git a/parser/prettyprinter.go b/parser/prettyprinter.go index a2a29c6..2b2c246 100644 --- a/parser/prettyprinter.go +++ b/parser/prettyprinter.go @@ -11,15 +11,22 @@ package parser import ( + "bufio" "bytes" "fmt" "strconv" + "strings" "text/template" "devt.de/krotik/common/errorutil" "devt.de/krotik/common/stringutil" ) +/* +IndentationLevel is the level of indentation which the pretty printer should use +*/ +const IndentationLevel = 4 + /* Map of AST nodes corresponding to lexer tokens */ @@ -126,7 +133,7 @@ func init() { // Loop statement - NodeLOOP + "_2": template.Must(template.New(NodeLOOP).Parse("for {{.c1}} {\n{{.c2}}}\n")), + NodeLOOP + "_2": template.Must(template.New(NodeLOOP).Parse("for {{.c1}} {\n{{.c2}}}")), NodeBREAK: template.Must(template.New(NodeBREAK).Parse("break")), NodeCONTINUE: template.Must(template.New(NodeCONTINUE).Parse("continue")), @@ -153,13 +160,13 @@ func init() { PrettyPrint produces pretty printed code from a given AST. */ func PrettyPrint(ast *ASTNode) (string, error) { - var visit func(ast *ASTNode, level int) (string, error) + var visit func(ast *ASTNode, path []*ASTNode) (string, error) - visit = func(ast *ASTNode, level int) (string, error) { + visit = func(ast *ASTNode, path []*ASTNode) (string, error) { var buf bytes.Buffer if ast == nil { - return "", fmt.Errorf("Nil pointer in AST at level: %v", level) + return "", fmt.Errorf("Nil pointer in AST") } numChildren := len(ast.Children) @@ -171,7 +178,7 @@ func PrettyPrint(ast *ASTNode) (string, error) { if numChildren > 0 { for i, child := range ast.Children { - res, err := visit(child, level+1) + res, err := visit(child, append(path, child)) if err != nil { return "", err } @@ -190,11 +197,13 @@ func PrettyPrint(ast *ASTNode) (string, error) { tempKey += fmt.Sprint("_", len(tempParam)) } - if res, ok := ppSpecialDefs(ast, level, tempParam, &buf); ok { + if res, ok := ppSpecialDefs(ast, path, tempParam, &buf); ok { return res, nil - } else if res, ok := ppSpecialBlocks(ast, level, tempParam, &buf); ok { + } else if res, ok := ppSpecialBlocks(ast, path, tempParam, &buf); ok { return res, nil - } else if res, ok := ppSpecialStatements(ast, level, tempParam, &buf); ok { + } else if res, ok := ppContainerBlocks(ast, path, tempParam, &buf); ok { + return res, nil + } else if res, ok := ppSpecialStatements(ast, path, tempParam, &buf); ok { return res, nil } @@ -217,26 +226,92 @@ func PrettyPrint(ast *ASTNode) (string, error) { errorutil.AssertOk(temp.Execute(&buf, tempParam)) - return ppMetaData(ast, buf.String()), nil + return ppPostProcessing(ast, path, buf.String()), nil } - return visit(ast, 0) + res, err := visit(ast, []*ASTNode{ast}) + + return strings.TrimSpace(res), err } /* -ppMetaData pretty prints meta data. +ppPostProcessing applies post processing rules. */ -func ppMetaData(ast *ASTNode, ppString string) string { +func ppPostProcessing(ast *ASTNode, path []*ASTNode, ppString string) string { ret := ppString // Add meta data if len(ast.Meta) > 0 { + for _, meta := range ast.Meta { + metaValue := meta.Value() if meta.Type() == MetaDataPreComment { - ret = fmt.Sprintf("/*%v*/ %v", meta.Value(), ret) + var buf bytes.Buffer + + scanner := bufio.NewScanner(strings.NewReader(metaValue)) + for scanner.Scan() { + buf.WriteString(fmt.Sprintf(" %v\n", strings.TrimSpace(scanner.Text()))) + } + buf.Truncate(buf.Len() - 1) // Remove the last newline + + if strings.Index(buf.String(), "\n") == -1 { + buf.WriteString(" ") + } + + ret = fmt.Sprintf("/*%v*/\n%v", buf.String(), ret) + } else if meta.Type() == MetaDataPostComment { - ret = fmt.Sprintf("%v #%v", ret, meta.Value()) + metaValue = strings.TrimSpace(strings.ReplaceAll(metaValue, "\n", "")) + ret = fmt.Sprintf("%v # %v", ret, metaValue) + } + } + } + + // Apply indentation + + if len(path) > 1 { + if stringutil.IndexOf(ast.Name, []string{ + NodeSTATEMENTS, + NodeMAP, + NodeLIST, + NodeKINDMATCH, + NodeSTATEMATCH, + NodeSCOPEMATCH, + NodePRIORITY, + NodeSUPPRESSES, + }) != -1 { + parent := path[len(path)-2] + + indentSpaces := stringutil.GenerateRollingString(" ", IndentationLevel) + ret = strings.ReplaceAll(ret, "\n", "\n"+indentSpaces) + + // Add initial indent only if we are inside a block statement + + if stringutil.IndexOf(parent.Name, []string{ + NodeASSIGN, + NodePRESET, + NodeKVP, + NodeLIST, + NodeFUNCCALL, + NodeKINDMATCH, + NodeSTATEMATCH, + NodeSCOPEMATCH, + NodePRIORITY, + NodeSUPPRESSES, + }) == -1 { + ret = fmt.Sprintf("%v%v", indentSpaces, ret) + } + + // Remove indentation from last line unless we have a special case + + if stringutil.IndexOf(parent.Name, []string{ + NodeSINK, + }) == -1 || ast.Name == NodeSTATEMENTS { + + if idx := strings.LastIndex(ret, "\n"); idx != -1 { + ret = ret[:idx+1] + ret[idx+IndentationLevel+1:] + } } } } @@ -247,7 +322,7 @@ func ppMetaData(ast *ASTNode, ppString string) string { /* ppSpecialDefs pretty prints special cases. */ -func ppSpecialDefs(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { +func ppSpecialDefs(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { numChildren := len(ast.Children) if ast.Name == NodeFUNCCALL { @@ -259,7 +334,7 @@ func ppSpecialDefs(ast *ASTNode, level int, tempParam map[string]string, buf *by } } - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodeSINK { @@ -268,7 +343,6 @@ func ppSpecialDefs(ast *ASTNode, level int, tempParam map[string]string, buf *by buf.WriteString("\n") for i := 1; i < len(ast.Children)-1; i++ { - buf.WriteString(" ") buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) buf.WriteString("\n") } @@ -277,57 +351,97 @@ func ppSpecialDefs(ast *ASTNode, level int, tempParam map[string]string, buf *by buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))]) buf.WriteString("}\n") - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } return "", false } /* -ppSpecialBlocks pretty prints special cases. +ppContainerBlocks pretty prints container structures. */ -func ppSpecialBlocks(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { +func ppContainerBlocks(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { numChildren := len(ast.Children) - // Handle special cases - children in tempParam have been resolved - - if stringutil.IndexOf(ast.Name, []string{NodeSTATEMENTS}) != -1 { - - // For statements just concat all children + if ast.Name == NodeLIST { + multilineThreshold := 4 + buf.WriteString("[") - for i := 0; i < numChildren; i++ { - buf.WriteString(stringutil.GenerateRollingString(" ", level*4)) - buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) + if numChildren > multilineThreshold { buf.WriteString("\n") } - return ppMetaData(ast, buf.String()), true + for i := 0; i < numChildren; i++ { - } else if ast.Name == NodeLIST { + buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) - buf.WriteString("[") - i := 1 - for ; i < numChildren; i++ { - buf.WriteString(tempParam[fmt.Sprint("c", i)]) - buf.WriteString(", ") + if i < numChildren-1 { + if numChildren > multilineThreshold { + buf.WriteString(",") + } else { + buf.WriteString(", ") + } + } + if numChildren > multilineThreshold { + buf.WriteString("\n") + } } - buf.WriteString(tempParam[fmt.Sprint("c", i)]) + buf.WriteString("]") - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodeMAP { - + multilineThreshold := 2 buf.WriteString("{") - i := 1 - for ; i < numChildren; i++ { - buf.WriteString(tempParam[fmt.Sprint("c", i)]) - buf.WriteString(", ") + + if numChildren > multilineThreshold { + buf.WriteString("\n") } - buf.WriteString(tempParam[fmt.Sprint("c", i)]) + + for i := 0; i < numChildren; i++ { + + buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) + + if i < numChildren-1 { + if numChildren > multilineThreshold { + buf.WriteString(",") + } else { + buf.WriteString(", ") + } + } + if numChildren > multilineThreshold { + buf.WriteString("\n") + } + } + buf.WriteString("}") - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true + + } + + return "", false +} + +/* +ppSpecialBlocks pretty prints special cases. +*/ +func ppSpecialBlocks(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { + numChildren := len(ast.Children) + + // Handle special cases - children in tempParam have been resolved + + if ast.Name == NodeSTATEMENTS { + + // For statements just concat all children + + for i := 0; i < numChildren; i++ { + buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) + buf.WriteString("\n") + } + + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodeTRY { @@ -340,9 +454,7 @@ func ppSpecialBlocks(ast *ASTNode, level int, tempParam map[string]string, buf * buf.WriteString(tempParam[fmt.Sprint("c", i+1)]) } - buf.WriteString("\n") - - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodeEXCEPT { @@ -361,7 +473,7 @@ func ppSpecialBlocks(ast *ASTNode, level int, tempParam map[string]string, buf * buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))]) buf.WriteString("}") - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } return "", false @@ -370,7 +482,7 @@ func ppSpecialBlocks(ast *ASTNode, level int, tempParam map[string]string, buf * /* ppSpecialStatements pretty prints special cases. */ -func ppSpecialStatements(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { +func ppSpecialStatements(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) { numChildren := len(ast.Children) if ast.Name == NodeIDENTIFIER { @@ -390,7 +502,7 @@ func ppSpecialStatements(ast *ASTNode, level int, tempParam map[string]string, b } } - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodePARAMS { @@ -403,7 +515,7 @@ func ppSpecialStatements(ast *ASTNode, level int, tempParam map[string]string, b buf.WriteString(tempParam[fmt.Sprint("c", i)]) buf.WriteString(")") - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } else if ast.Name == NodeIF { @@ -429,9 +541,7 @@ func ppSpecialStatements(ast *ASTNode, level int, tempParam map[string]string, b } } - buf.WriteString("\n") - - return ppMetaData(ast, buf.String()), true + return ppPostProcessing(ast, path, buf.String()), true } return "", false diff --git a/parser/prettyprinter_test.go b/parser/prettyprinter_test.go index a2f0fdd..346a3ad 100644 --- a/parser/prettyprinter_test.go +++ b/parser/prettyprinter_test.go @@ -29,7 +29,7 @@ func TestErrorHandling(t *testing.T) { astres.Children[1].Children[1] = nil ppres, err := PrettyPrint(astres) - if err == nil || err.Error() != "Nil pointer in AST at level: 2" { + if err == nil || err.Error() != "Nil pointer in AST" { t.Errorf("Unexpected result: %v error: %v", ppres, err) return } @@ -207,3 +207,323 @@ or return } } + +func TestSpecialCasePrinting1(t *testing.T) { + input := `a := {"a":1,"b":1,"c":1,"d" : 1, "e":1,"f":1,"g":1,"h":1,}` + + if err := UnitTestPrettyPrinting(input, "", + `a := { + "a" : 1, + "b" : 1, + "c" : 1, + "d" : 1, + "e" : 1, + "f" : 1, + "g" : 1, + "h" : 1 +}`); err != nil { + t.Error(err) + return + } + + input = `a := {"a":1,"b":1,"c":1,"d" : {"a":1,"b":{"a":1,"b":1,"c":1,"d":1},"c":1,"d" : 1, "e":1,"f":{"a":1,"b":1},"g":1,"h":1,}, "e":1,"f":1,"g":1,"h":1,}` + + if err := UnitTestPrettyPrinting(input, "", + `a := { + "a" : 1, + "b" : 1, + "c" : 1, + "d" : { + "a" : 1, + "b" : { + "a" : 1, + "b" : 1, + "c" : 1, + "d" : 1 + }, + "c" : 1, + "d" : 1, + "e" : 1, + "f" : {"a" : 1, "b" : 1}, + "g" : 1, + "h" : 1 + }, + "e" : 1, + "f" : 1, + "g" : 1, + "h" : 1 +}`); err != nil { + t.Error(err) + return + } + + input = `a := [1,2,3,[1,2,[1,2],3,[1,2,3,4],[1,2,3,4,5],4,5],4,5]` + + if err := UnitTestPrettyPrinting(input, "", + `a := [ + 1, + 2, + 3, + [ + 1, + 2, + [1, 2], + 3, + [1, 2, 3, 4], + [ + 1, + 2, + 3, + 4, + 5 + ], + 4, + 5 + ], + 4, + 5 +]`); err != nil { + t.Error(err) + return + } + + input = `a := [1,2,3,[1,2,{"a":1,"b":1,"c":1,"d":1},3,[1,2,3,4],4,5],4,5]` + + if err := UnitTestPrettyPrinting(input, "", + `a := [ + 1, + 2, + 3, + [ + 1, + 2, + { + "a" : 1, + "b" : 1, + "c" : 1, + "d" : 1 + }, + 3, + [1, 2, 3, 4], + 4, + 5 + ], + 4, + 5 +]`); err != nil { + t.Error(err) + return + } + +} + +func TestSpecialCasePrinting2(t *testing.T) { + input := ` +a := 1 +a := 2 +sink RegisterNewPlayer + kindmatch ["foo",2] + statematch {"a":1,"b":1,"c":1,"d":1} +scopematch [] +suppresses ["abs"] +priority 0 +{ +log("1223") +log("1223") +func foo (z=[1,2,3,4,5]) { +a := 1 +b := 2 +} +log("1223") +try { +x := [1,2,3,4] + raise("test 12", null, [1,2,3]) +} except e { +p := 1 +} +} +` + + if err := UnitTestPrettyPrinting(input, "", + `a := 1 +a := 2 +sink RegisterNewPlayer + kindmatch ["foo", 2] + statematch { + "a" : 1, + "b" : 1, + "c" : 1, + "d" : 1 + } + scopematch [] + suppresses ["abs"] + priority 0 +{ + log("1223") + log("1223") + func foo(z=[ + 1, + 2, + 3, + 4, + 5 + ]) { + a := 1 + b := 2 + } + log("1223") + try { + x := [1, 2, 3, 4] + raise("test 12", null, [1, 2, 3]) + } except e { + p := 1 + } +}`); err != nil { + t.Error(err) + } + + input = ` + /* + + Some initial comment + + bla + */ +a := 1 +func aaa() { +mutex myresource { + globalResource := "new value" +} +func myfunc(a, b, c=1) { + a := 1 + 1 # Test +} +x := [ 1,2,3,4,5] +a:=1;b:=1 +/*Foo*/ +Foo := { + "super" : [ Bar ] + + /* + * Object IDs + */ + "id" : 0 # aaaa + "idx" : 0 + +/*Constructor*/ + "init" : func(id) +{ + super[0]() + this.id := id + } + + /* + Return the object ID + */ + "getId" : func() { + return this.idx + } + + /* + Set the object ID + */ + "setId" : func(id) { + this.idx := id + } +} +for a in range(2, 10, 2) { + a := 1 +} +for a > 0 { + a := 1 +} +if a == 1 { + a := a + 1 +} elif a == 2 { + a := a + 2 +} else { + a := 99 +} +try { + raise("MyError", "My error message", [1,2,3]) +} except "MyError" as e { + log(e) +} +} +b:=1 +` + + if err := UnitTestPrettyPrinting(input, "", + `/* + + Some initial comment + + bla + */ +a := 1 +func aaa() { + mutex myresource { + globalResource := "new value" + } + + func myfunc(a, b, c=1) { + a := 1 + 1 # Test + } + x := [ + 1, + 2, + 3, + 4, + 5 + ] + a := 1 + b := 1 + /* Foo */ + Foo := { + "super" : [Bar], + /* + * Object IDs + */ + "id" : 0 # aaaa, + "idx" : 0, + /* Constructor */ + "init" : func (id) { + super[0]() + this.id := id + }, + /* + Return the object ID + */ + "getId" : func () { + return this.idx + }, + /* + Set the object ID + */ + "setId" : func (id) { + this.idx := id + } + } + for a in range(2, 10, 2) { + a := 1 + } + for a > 0 { + a := 1 + } + if a == 1 { + a := a + 1 + } elif a == 2 { + a := a + 2 + } else { + a := 99 + } + try { + raise("MyError", "My error message", [1, 2, 3]) + } except "MyError" as e { + log(e) + } +} +b := 1`); err != nil { + t.Error(err) + return + } +} diff --git a/scope/helper.go b/scope/helper.go index 86291f8..0cb3ec5 100644 --- a/scope/helper.go +++ b/scope/helper.go @@ -63,3 +63,61 @@ func ToScope(name string, o map[interface{}]interface{}) parser.Scope { } return vs } + +/* +ConvertJSONToECALObject converts a JSON container structure into an object which +can be used by ECAL. +*/ +func ConvertJSONToECALObject(v interface{}) interface{} { + res := v + + if mapContainer, ok := v.(map[string]interface{}); ok { + newRes := make(map[interface{}]interface{}) + + for mk, mv := range mapContainer { + newRes[mk] = ConvertJSONToECALObject(mv) + } + + res = newRes + + } else if mapList, ok := v.([]interface{}); ok { + newRes := make([]interface{}, len(mapList)) + + for i, lv := range mapList { + newRes[i] = ConvertJSONToECALObject(lv) + } + + res = newRes + } + + return res +} + +/* +ConvertECALToJSONObject converts an ECAL container structure into an object which +can be marshalled into a JSON string. +*/ +func ConvertECALToJSONObject(v interface{}) interface{} { + res := v + + if mapContainer, ok := v.(map[interface{}]interface{}); ok { + newRes := make(map[string]interface{}) + + for mk, mv := range mapContainer { + newRes[fmt.Sprint(mk)] = ConvertECALToJSONObject(mv) + } + + res = newRes + + } else if mapList, ok := v.([]interface{}); ok { + newRes := make([]interface{}, len(mapList)) + + for i, lv := range mapList { + newRes[i] = ConvertECALToJSONObject(lv) + } + + res = newRes + } + + return res +} diff --git a/scope/helper_test.go b/scope/helper_test.go index 52fbc2e..b37e0ad 100644 --- a/scope/helper_test.go +++ b/scope/helper_test.go @@ -11,6 +11,7 @@ package scope import ( + "fmt" "testing" "devt.de/krotik/ecal/parser" @@ -39,3 +40,31 @@ func TestScopeConversion(t *testing.T) { return } } + +func TestConvertJSONToECALObject(t *testing.T) { + + testJSONStructure := map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": "123", + }, + }, + } + + res := ConvertJSONToECALObject(testJSONStructure) + + if typeString := fmt.Sprintf("%#v", res); typeString != + `map[interface {}]interface {}{"foo":[]interface {}{map[interface {}]interface {}{"bar":"123"}}}` { + t.Error("Unexpected result:", typeString) + return + } + + res = ConvertECALToJSONObject(res) + + if typeString := fmt.Sprintf("%#v", res); typeString != + `map[string]interface {}{"foo":[]interface {}{map[string]interface {}{"bar":"123"}}}` { + t.Error("Unexpected result:", typeString) + return + } + +}