From a940c309036d8673b44251393a21ecbe5fbcfb87 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 19 May 2017 18:56:39 -0700 Subject: [PATCH] Mechanism for introspection of source code for editors The new "Nav" member on a zcl.File is an opaque object that can be populated by parsers with an object that supports certain interfaces that are not part of the main API but are useful for integration with editors and other tooling. As a first example of this, we replace the hack for getting context in the diagnostic package with a new ContextString interface, which can then be optionally implemented by a given parser to return a contextual string native to the source language. --- zcl/diagnostic_text.go | 51 ++++++++++++++++++++++--------------- zcl/diagnostic_text_test.go | 22 +++++++++++----- zcl/structure.go | 5 ++++ zcled/doc.go | 4 +++ zcled/navigation.go | 20 +++++++++++++++ zclparse/parser.go | 10 ++++++++ 6 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 zcled/doc.go create mode 100644 zcled/navigation.go diff --git a/zcl/diagnostic_text.go b/zcl/diagnostic_text.go index 1bdd39d..7a455b7 100644 --- a/zcl/diagnostic_text.go +++ b/zcl/diagnostic_text.go @@ -6,16 +6,15 @@ import ( "errors" "fmt" "io" - "strings" wordwrap "github.com/mitchellh/go-wordwrap" ) type diagnosticTextWriter struct { - sources map[string][]byte - wr io.Writer - width uint - color bool + files map[string]*File + wr io.Writer + width uint + color bool } // NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics @@ -30,12 +29,12 @@ type diagnosticTextWriter struct { // If color is set to true, the output will include VT100 escape sequences to // color-code the severity indicators. It is suggested to turn this off if // the target writer is not a terminal. -func NewDiagnosticTextWriter(wr io.Writer, sources map[string][]byte, width uint, color bool) DiagnosticWriter { +func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter { return &diagnosticTextWriter{ - sources: sources, - wr: wr, - width: width, - color: color, + files: files, + wr: wr, + width: width, + color: color, } } @@ -70,14 +69,14 @@ func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error { if diag.Subject != nil { - src := w.sources[diag.Subject.Filename] - if src == nil { + file := w.files[diag.Subject.Filename] + if file == nil || file.Bytes == nil { fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line) } else { + src := file.Bytes r := bytes.NewReader(src) sc := bufio.NewScanner(r) sc.Split(bufio.ScanLines) - contextLine := "" var startLine, endLine int if diag.Context != nil { @@ -88,17 +87,18 @@ func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error { endLine = diag.Subject.End.Line } + var contextLine string + if diag.Subject != nil { + contextLine = contextString(file, diag.Subject.Start.Byte) + if contextLine != "" { + contextLine = ", in " + contextLine + } + } + li := 1 var ls string for sc.Scan() { ls = sc.Text() - if len(ls) > 0 && strings.Contains(ls, "{") && ls[0] != ' ' && ls[0] != '\t' && ls[0] != '#' && ls[0] != '/' && ls[0] != '{' { - // Keep track of the latest non-space-prefixed line we've - // seen, to use as context. This is a pretty sloppy way - // to do this, but it works well enough for most normal - // files. (In particular though, it doesn't work for JSON sources.) - contextLine = fmt.Sprintf(", in %s", strings.Replace(ls, " {", "", 1)) - } if li == startLine { break @@ -150,3 +150,14 @@ func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error { } return nil } + +func contextString(file *File, offset int) string { + type contextStringer interface { + ContextString(offset int) string + } + + if cser, ok := file.Nav.(contextStringer); ok { + return cser.ContextString(offset) + } + return "" +} diff --git a/zcl/diagnostic_text_test.go b/zcl/diagnostic_text_test.go index 6930bae..9d51731 100644 --- a/zcl/diagnostic_text_test.go +++ b/zcl/diagnostic_text_test.go @@ -31,7 +31,7 @@ func TestDiagnosticTextWriter(t *testing.T) { }, `Error: Splines not reticulated - on line 1: + on line 1, in hardcoded-context: 1: foo = 1 All splines must be pre-reticulated. @@ -56,7 +56,7 @@ All splines must be pre-reticulated. }, `Error: Unsupported attribute - on line 3: + on line 3, in hardcoded-context: 3: baz = 3 "baz" is not a supported top-level @@ -94,7 +94,7 @@ attribute. Did you mean "bam"? }, `Error: Unsupported attribute - on line 5, in block "party": + on line 5, in hardcoded-context: 4: block "party" { 5: pizza = "cheese" 6: } @@ -106,14 +106,17 @@ Did you mean "pizzetta"? }, } - sources := map[string][]byte{ - "": []byte(testDiagnosticTextWriterSource), + files := map[string]*File{ + "": &File{ + Bytes: []byte(testDiagnosticTextWriterSource), + Nav: &diagnosticTestNav{}, + }, } for i, test := range tests { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { bwr := &bytes.Buffer{} - dwr := NewDiagnosticTextWriter(bwr, sources, 40, false) + dwr := NewDiagnosticTextWriter(bwr, files, 40, false) err := dwr.WriteDiagnostic(test.Input) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -133,3 +136,10 @@ block "party" { pizza = "cheese" } ` + +type diagnosticTestNav struct { +} + +func (tn *diagnosticTestNav) ContextString(offset int) string { + return "hardcoded-context" +} diff --git a/zcl/structure.go b/zcl/structure.go index 1df01b0..d567052 100644 --- a/zcl/structure.go +++ b/zcl/structure.go @@ -8,6 +8,11 @@ import ( type File struct { Body Body Bytes []byte + + // Nav is used to integrate with the "zcled" editor integration package, + // and with diagnostic information formatters. It is not for direct use + // by a calling application. + Nav interface{} } // Block represents a nested block within a Body. diff --git a/zcled/doc.go b/zcled/doc.go new file mode 100644 index 0000000..9e77fd1 --- /dev/null +++ b/zcled/doc.go @@ -0,0 +1,4 @@ +// Package zcled provides functionality intended to help an application +// that embeds zcl to deliver relevant information to a text editor or IDE +// for navigating around and analyzing configuration files. +package zcled diff --git a/zcled/navigation.go b/zcled/navigation.go new file mode 100644 index 0000000..7190494 --- /dev/null +++ b/zcled/navigation.go @@ -0,0 +1,20 @@ +package zcled + +import ( + "github.com/apparentlymart/go-zcl/zcl" +) + +type contextStringer interface { + ContextString(offset int) string +} + +// ContextString returns a string describing the context of the given byte +// offset, if available. An empty string is returned if no such information +// is available, or otherwise the returned string is in a form that depends +// on the language used to write the referenced file. +func ContextString(file *zcl.File, offset int) string { + if cser, ok := file.Nav.(contextStringer); ok { + return cser.ContextString(offset) + } + return "" +} diff --git a/zclparse/parser.go b/zclparse/parser.go index 06c1234..360f921 100644 --- a/zclparse/parser.go +++ b/zclparse/parser.go @@ -65,3 +65,13 @@ func (p *Parser) Sources() map[string][]byte { } return ret } + +// Files returns a map from filenames to the File objects produced from them. +// This is intended to be used, for example, to print diagnostics with +// contextual information. +// +// The returned map and all of the objects it refers to directly or indirectly +// must not be modified. +func (p *Parser) Files() map[string]*zcl.File { + return p.files +}