├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── doc.go ├── error.go ├── error_test.go ├── example_test.go ├── fuzz ├── README.md └── main.go ├── position.go ├── position_test.go ├── source.go └── source_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /cover.out 2 | /Guardfile 3 | /locerr_fuzz-fuzz.zip 4 | /fuzz/corpus 5 | /fuzz/crashers 6 | /fuzz/suppressions 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.x 3 | os: 4 | - linux 5 | - osx 6 | 7 | before_install: 8 | - go get -t -v ./... 9 | 10 | script: 11 | - go test -coverprofile=coverage.txt -covermode=count ./ 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | the MIT License 2 | 3 | Copyright (c) 2017 rhysd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 20 | THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :x: locerr 2 | ========== 3 | [![Build Status][build badge]][travis result] 4 | [![Windows Build status][windows build badge]][appveyor result] 5 | [![Coverage Status][coverage status]][coverage result] 6 | [![GoDoc][godoc badge]][locerr document] 7 | 8 | [locerr][locerr document] is a small library to make a nice-looking locational error in a source code. 9 | It provides a struct to represent a source file, a specific position in code and an error related to 10 | specific range or position in source. 11 | 12 | This library is useful to provide a unified look for error messages raised by compilers, interpreters 13 | or translators. 14 | 15 | By using `locerr.Source` and `locerr.Pos` types as position information, this library can provide 16 | an error type which shows nice look error message. 17 | 18 | - It shows the code snippet which caused an error 19 | - Enable to add notes to error by nesting an error instance like [pkg/errors](https://github.com/pkg/errors) 20 | - Proper location is automatically added to error messages and notes 21 | - Colorized label like 'Error:' or 'Note:' 22 | - Windows is supported 23 | 24 | It's important to make a good error when compilation or execution errors found. [locerr][locerr document] 25 | helps it. This library is actually used in some my compiler implementation. 26 | 27 | ## Installation 28 | 29 | Please use `go get`. 30 | 31 | ```console 32 | $ go get -u github.com/rhysd/locerr 33 | ``` 34 | 35 | ## Usage 36 | 37 | As example, let's say to make a locational error for following pseudo code. In this code, function 38 | `foo` is defined with 1 parameter but called with 3 parameters. 39 | 40 | ``` 41 | function foo(x: bool): int { 42 | return (if x then 42 else 21) 43 | } 44 | 45 | function main() { 46 | foo(true, 47 | 42, 48 | "test") 49 | } 50 | ``` 51 | 52 | We can make a locational error with some notes using locerr as following. 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | "os" 60 | 61 | "github.com/rhysd/locerr" 62 | ) 63 | 64 | func main() { 65 | // At first you should gain entire source as *locerr.Source instance. 66 | 67 | code := 68 | `function foo(x: bool): int { 69 | return (if x then 42 else 21) 70 | } 71 | 72 | function main() { 73 | foo(true, 74 | 42, 75 | "test") 76 | }` 77 | src := locerr.NewDummySource(code) 78 | 79 | // You can get *locerr.Source instance from file (NewSourceFromFile) or stdin (NewSourceFromStdin) also. 80 | 81 | // Let's say to find an error at some range in the source. 'start' indicates the head of the first argument. 82 | // 'end' indicates the end of the last argument. 83 | 84 | start := locerr.Pos{ 85 | Offset: 88, 86 | Line: 6, 87 | Column: 7, 88 | File: src, 89 | } 90 | end := locerr.Pos{ 91 | Offset: 116, 92 | Line: 9, 93 | Column: 12, 94 | File: src, 95 | } 96 | 97 | // NewError or other factory functions make a new error instance with the range. locerr.Error instance 98 | // implements error interface so it can be handled like other error types. 99 | 100 | err := locerr.ErrorIn(start, end, "Calling 'foo' with wrong number of argument") 101 | 102 | // Assume that you find additional information (location of variable and its type). Then you can add some 103 | // notes to the error. Notes can be added by wrapping errors like pkg/errors library. 104 | 105 | prev := locerr.Pos{ 106 | Offset: 9, 107 | Line: 1, 108 | Column: 10, 109 | File: src, 110 | } 111 | 112 | err = err.NoteAt(prev, "Defined with 1 parameter") 113 | err = err.NoteAt(prev, "'foo' was defined as 'bool -> int'") 114 | 115 | // Finally you can see the result! 116 | 117 | // Get the error message as string. Note that this is only for non-Windows OS. 118 | fmt.Println(err) 119 | 120 | // Directly writes the error message into given file. 121 | // This supports Windows. Useful to output from stdout or stderr. 122 | err.PrintToFile(os.Stdout) 123 | } 124 | ``` 125 | 126 | Above code should show the following output: 127 | 128 | ``` 129 | Error: Calling 'foo' with wrong number of argument (at :6:7) 130 | Note: Defined with 1 parameter (at :1:10) 131 | Note: 'foo' was defined as 'bool -> int' (at :1:10) 132 | 133 | > foo(true, 134 | > 42, 135 | > "test") 136 | 137 | ``` 138 | 139 | output screenshot 140 | 141 | Labels such as 'Error:' or 'Notes:' are colorized. Main error message is emphasized with bold font. 142 | And source code location information (file name, line and column) is added with gray text. 143 | If the error has range information, the error shows code snippet which caused the error at the end 144 | of error message. 145 | 146 | If you have only one position information rather than two, 'start' position and 'end' position, 147 | `ErrorAt` is available instead of `ErrorIn`. `ErrorAt` takes one `Pos` instance. 148 | 149 | ```go 150 | err := locerr.ErrorAt(start, "Calling 'foo' with wrong number of argument") 151 | ``` 152 | 153 | In this case, line snippet is shown in error message. `pos.Line` is used to get line from source text. 154 | `fmt.Println(err)` will show the following. 155 | 156 | ``` 157 | Error: Calling 'foo' with wrong number of argument (at :6:7) 158 | 159 | > foo(true, 160 | 161 | ``` 162 | 163 | 164 | ## Development 165 | 166 | ### How to run tests 167 | 168 | ```console 169 | $ go test ./ 170 | ``` 171 | 172 | Note that `go test -v` may fail because color sequences are not assumed in tests. 173 | 174 | ### How to run fuzzing test 175 | 176 | Fuzzing test using [go-fuzz][]. 177 | 178 | ```console 179 | $ cd ./fuzz 180 | $ go-fuzz-build github.com/rhysd/locerr/fuzz 181 | $ go-fuzz -bin=./locerr_fuzz-fuzz.zip -workdir=fuzz 182 | ``` 183 | 184 | Last command starts fuzzing tests until stopped with `^C`. Every 3 seconds it reports the current 185 | result. It makes 3 directories in `fuzz` directory as the result, `corpus`, `crashers` and 186 | `suppressions`. `crashers` contains the information about the crash caused by fuzzing. 187 | 188 | [locerr document]: https://godoc.org/github.com/rhysd/locerr 189 | [build badge]: https://travis-ci.org/rhysd/locerr.svg?branch=master 190 | [travis result]: https://travis-ci.org/rhysd/locerr 191 | [coverage status]: https://codecov.io/gh/rhysd/locerr/branch/master/graph/badge.svg 192 | [coverage result]: https://codecov.io/gh/rhysd/locerr 193 | [windows build badge]: https://ci.appveyor.com/api/projects/status/v4ghlgka6e6st2mn/branch/master?svg=true 194 | [appveyor result]: https://ci.appveyor.com/project/rhysd/locerr/branch/master 195 | [godoc badge]: https://godoc.org/github.com/rhysd/locerr?status.svg 196 | [go-fuzz]: https://github.com/dvyukov/go-fuzz 197 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | clone_depth: 1 3 | clone_folder: c:\gopath\src\github.com\rhysd\loc 4 | environment: 5 | GOPATH: c:\gopath 6 | 7 | install: 8 | - echo %PATH% 9 | - echo %GOPATH% 10 | - go version 11 | - go env 12 | - go get -v -t ./... 13 | 14 | build: off 15 | 16 | test_script: 17 | - go test -coverprofile=coverage.txt -covermode=count ./ 18 | 19 | after_test: 20 | - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" 21 | - pip install codecov 22 | - codecov -f "coverage.txt" 23 | 24 | deploy: off 25 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package locerr is a small library to make an error with source code location information. 3 | It provides a struct to represent a source file, a specific position in 4 | code and an error related to specific range or position in source. 5 | 6 | It's important to make a good error when compilation or execution errors found. locerr helps it. 7 | This library is actually used in some my compiler implementation. 8 | 9 | Repository: https://github.com/rhysd/locerr 10 | 11 | At first you should gain entire source as *Source instance. 12 | 13 | code := 14 | `package main 15 | 16 | func main() { 17 | foo := 42 18 | 19 | foo := true 20 | } 21 | ` 22 | src := locerr.NewDummySource(code) 23 | 24 | You can get *Source instance from file (NewSourceFromFile) or stdin (NewSourceFromStdin) also. 25 | 26 | Let's say to find an error at some range in the source. 27 | 28 | start := locerr.Pos{ 29 | Offset: 41, 30 | Line: 6, 31 | Column: 2, 32 | File: src, 33 | } 34 | end := locerr.Pos{ 35 | Offset: 52, 36 | Line: 6, 37 | Column: 12, 38 | File: src, 39 | } 40 | 41 | ErrorIn or other factory functions make a new error instance with the range. Error instance implements 42 | error interface so it can be handled like other error types. 43 | 44 | err := locerr.ErrorIn(start, end, "Found duplicate symbol 'foo'") 45 | 46 | Assume that you find additional information (location of variable and its type). Then you can add some 47 | notes to the error. Notes can be added by wrapping errors like pkg/errors library. 48 | 49 | prev := locerr.Pos{ 50 | Offset: 26, 51 | Line: 4, 52 | Column: 1, 53 | File: src, 54 | } 55 | 56 | err = err.NoteAt(prev, "Defined here at first") 57 | err = err.NoteAt(prev, "Previously defined as int") 58 | 59 | Finally you can see the result! err.Error() gets the error message as string. Note that this is only for 60 | non-Windows OS. 61 | 62 | fmt.Println(err) 63 | 64 | It should output following: 65 | 66 | Error: Found duplicate symbol 'foo' (at :6:1) 67 | Note: Defined here at first (at :4:1) 68 | Note: Previously defined as int (at :4:1) 69 | 70 | > foo := true 71 | 72 | 73 | To support Windows, please use PrintToFile() method. It directly writes the error message into given file. 74 | This supports Windows and is useful to output from stdout or stderr. 75 | 76 | err.PrintToFile(os.Stderr) 77 | 78 | Labels such as 'Error:' or 'Notes:' are colorized. Main error message is emphasized with bold font. 79 | And source code location information (file name, line and column) is added with gray text. 80 | If the error has range information, the error shows code snippet which caused the error at the end 81 | of error message 82 | 83 | Colorized output can be seen at https://github.com/rhysd/ss/blob/master/locerr/output.png?raw=true 84 | 85 | If you have only one position information rather than two, 'start' position and 'end' position, 86 | ErrorAt() is available instead of ErrorIn() ErrorAt() takes one Pos instance. 87 | 88 | err = ErrorAt(start, "Calling 'foo' with wrong number of argument") 89 | 90 | In this case, line snippet is shown in error message. `pos.Line` is used to get line from source text. 91 | 92 | fmt.Println(err) 93 | 94 | It should output following: 95 | 96 | Output: 97 | Error: Calling 'foo' with wrong number of argument (at :6:7) 98 | 99 | > foo(true, 100 | 101 | 102 | */ 103 | package locerr 104 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "github.com/mattn/go-colorable" 12 | ) 13 | 14 | // SetColor controls font should be colorful or not. 15 | func SetColor(enabled bool) { 16 | color.NoColor = !enabled 17 | } 18 | 19 | var ( 20 | bold = color.New(color.Bold) 21 | red = color.New(color.FgRed) 22 | green = color.New(color.FgGreen) 23 | gray = color.New(color.FgHiBlack) 24 | emphasis = color.New(color.FgHiGreen, color.Bold, color.Underline) 25 | ) 26 | 27 | // Error represents a compilation error with positional information and stacked messages. 28 | type Error struct { 29 | Start Pos 30 | End Pos 31 | Messages []string 32 | } 33 | 34 | func writeSnipLine(w io.Writer, line string) { 35 | indent, len := 0, len(line) 36 | for indent < len { 37 | if line[indent] != ' ' && line[indent] != '\t' { 38 | break 39 | } 40 | indent++ 41 | } 42 | if indent != 0 { 43 | // Write indent without emphasis 44 | fmt.Fprint(w, line[:indent]) 45 | } 46 | if indent != len { 47 | // Write code snip with emphasis 48 | emphasis.Fprint(w, line[indent:]) 49 | } 50 | } 51 | 52 | func (err *Error) writeSnip(w io.Writer) { 53 | fmt.Fprint(w, "\n\n> ") 54 | 55 | code := err.Start.File.Code 56 | start := err.Start.Offset 57 | for start-1 >= 0 { 58 | if code[start-1] == '\n' { 59 | break 60 | } 61 | start-- 62 | } 63 | if start < err.Start.Offset { 64 | // Write code before snip in first line 65 | w.Write(code[start:err.Start.Offset]) 66 | } 67 | 68 | lines := strings.Split(string(code[err.Start.Offset:err.End.Offset]), "\n") 69 | 70 | // First line does not have "> " prefix 71 | writeSnipLine(w, lines[0]) 72 | 73 | for _, line := range lines[1:] { 74 | fmt.Fprint(w, "\n> ") 75 | writeSnipLine(w, line) 76 | } 77 | 78 | end := err.End.Offset 79 | len := len(code) 80 | for end < len { 81 | if code[end] == '\n' { 82 | break 83 | } 84 | end++ 85 | } 86 | if err.End.Offset < end { 87 | // Write code after snip in last line 88 | w.Write(code[err.End.Offset:end]) 89 | } 90 | 91 | fmt.Fprint(w, "\n") 92 | 93 | // TODO: 94 | // If the code snippet for the token is too long, skip lines with '...' except for starting N lines 95 | // and ending N lines 96 | } 97 | 98 | func lineStartOffset(code []byte, lnum int) int { 99 | l := 1 100 | for i, r := range code { 101 | if l == lnum { 102 | return i 103 | } 104 | if r == '\n' { 105 | l++ 106 | } 107 | } 108 | return -1 109 | } 110 | 111 | // Show line based on err.Start.Line. We don't use offset for this because some environment offset 112 | // cannot be obtained (e.g. getting location from runtime.Caller). 113 | func (err *Error) writeOnelineSnip(w io.Writer) { 114 | code := err.Start.File.Code 115 | len := len(code) 116 | if len == 0 { 117 | return 118 | } 119 | 120 | start := lineStartOffset(code, err.Start.Line) 121 | if start == -1 { 122 | return 123 | } 124 | 125 | end := start 126 | for end < len { 127 | if code[end] == '\n' { 128 | break 129 | } 130 | end++ 131 | } 132 | 133 | if start == end { 134 | // Snippet is empty. Skipped. 135 | return 136 | } 137 | 138 | fmt.Fprint(w, "\n\n> ") 139 | w.Write(code[start:end]) 140 | w.Write([]byte{'\n'}) 141 | } 142 | 143 | // WriteMessage writes error message to the given writer 144 | func (err *Error) WriteMessage(w io.Writer) { 145 | // Error: {msg} (at {pos}) 146 | // {note1} 147 | // {note2} 148 | // ... 149 | red.Fprint(w, "Error: ") 150 | bold.Fprint(w, err.Messages[0]) 151 | if err.Start.File != nil { 152 | gray.Fprintf(w, " (at %s)", err.Start.String()) 153 | } 154 | for _, msg := range err.Messages[1:] { 155 | green.Fprint(w, "\n Note: ") 156 | fmt.Fprint(w, msg) 157 | } 158 | 159 | if err.Start.File == nil { 160 | return 161 | } 162 | if err.End.File == nil || err.Start.Offset == err.End.Offset { 163 | err.writeOnelineSnip(w) 164 | return 165 | } 166 | err.writeSnip(w) 167 | } 168 | 169 | // Error builds error message for the error. 170 | func (err *Error) Error() string { 171 | var buf bytes.Buffer 172 | err.WriteMessage(&buf) 173 | return buf.String() 174 | } 175 | 176 | // PrintToFile prints error message to the given file. This is useful on Windows because Error() 177 | // does not support colorful string on Windows. 178 | func (err *Error) PrintToFile(f *os.File) { 179 | err.WriteMessage(colorable.NewColorable(f)) 180 | } 181 | 182 | // Note stacks the additional message upon current error. 183 | func (err *Error) Note(msg string) *Error { 184 | err.Messages = append(err.Messages, msg) 185 | return err 186 | } 187 | 188 | // Notef stacks the additional formatted message upon current error. 189 | func (err *Error) Notef(format string, args ...interface{}) *Error { 190 | err.Messages = append(err.Messages, fmt.Sprintf(format, args...)) 191 | return err 192 | } 193 | 194 | // NoteAt stacks the additional message upon current error with position. 195 | func (err *Error) NoteAt(pos Pos, msg string) *Error { 196 | at := color.HiBlackString("(at %s)", pos.String()) 197 | err.Messages = append(err.Messages, fmt.Sprintf("%s %s", msg, at)) 198 | return err 199 | } 200 | 201 | // NotefAt stacks the additional formatted message upon current error with poisition. 202 | func (err *Error) NotefAt(pos Pos, format string, args ...interface{}) *Error { 203 | return err.NoteAt(pos, fmt.Sprintf(format, args...)) 204 | } 205 | 206 | // In sets start and end positions of the error. 207 | func (err *Error) In(start, end Pos) *Error { 208 | err.Start = start 209 | err.End = end 210 | return err 211 | } 212 | 213 | // At sets a position where error occurred. 214 | func (err *Error) At(pos Pos) *Error { 215 | err.Start = pos 216 | err.End = Pos{} 217 | return err 218 | } 219 | 220 | // NewError makes locerr.Error instance without source location information. 221 | func NewError(msg string) *Error { 222 | return &Error{Pos{}, Pos{}, []string{msg}} 223 | } 224 | 225 | // ErrorIn makes a new compilation error with the range. 226 | func ErrorIn(start, end Pos, msg string) *Error { 227 | return &Error{start, end, []string{msg}} 228 | } 229 | 230 | // ErrorAt makes a new compilation error with the position. 231 | func ErrorAt(pos Pos, msg string) *Error { 232 | return ErrorIn(pos, Pos{}, msg) 233 | } 234 | 235 | // Errorf makes locerr.Error instance without source location information following given format. 236 | func Errorf(format string, args ...interface{}) *Error { 237 | return NewError(fmt.Sprintf(format, args...)) 238 | } 239 | 240 | // ErrorfIn makes a new compilation error with the range and formatted message. 241 | func ErrorfIn(start, end Pos, format string, args ...interface{}) *Error { 242 | return ErrorIn(start, end, fmt.Sprintf(format, args...)) 243 | } 244 | 245 | // ErrorfAt makes a new compilation error with the position and formatted message. 246 | func ErrorfAt(pos Pos, format string, args ...interface{}) *Error { 247 | return ErrorIn(pos, Pos{}, fmt.Sprintf(format, args...)) 248 | } 249 | 250 | // WithRange adds range information to the passed error. 251 | func WithRange(start, end Pos, err error) *Error { 252 | return ErrorIn(start, end, err.Error()) 253 | } 254 | 255 | // WithPos adds positional information to the passed error. 256 | func WithPos(pos Pos, err error) *Error { 257 | return ErrorAt(pos, err.Error()) 258 | } 259 | 260 | // Note adds note to the given error. If given error is not locerr.Error, it's converted into locerr.Error. 261 | func Note(err error, msg string) *Error { 262 | if err, ok := err.(*Error); ok { 263 | return err.Note(msg) 264 | } 265 | return &Error{Pos{}, Pos{}, []string{err.Error(), msg}} 266 | } 267 | 268 | // NoteIn adds range information and stack additional message to the original error. If given error is not locerr.Error, it's converted into locerr.Error. 269 | func NoteIn(start, end Pos, err error, msg string) *Error { 270 | if err, ok := err.(*Error); ok { 271 | return err.NoteAt(start, msg) 272 | } 273 | return &Error{start, end, []string{err.Error(), msg}} 274 | } 275 | 276 | // NoteAt adds positional information and stack additional message to the original error. If given error is not locerr.Error, it's converted into locerr.Error. 277 | func NoteAt(pos Pos, err error, msg string) *Error { 278 | return NoteIn(pos, Pos{}, err, msg) 279 | } 280 | 281 | // Notef adds note to the given error. Description will be created following given format and arguments. If given error is not locerr.Error, it's converted into locerr.Error. 282 | func Notef(err error, format string, args ...interface{}) *Error { 283 | return Note(err, fmt.Sprintf(format, args...)) 284 | } 285 | 286 | // NotefIn adds range information and stack additional formatted message to the original error. If given error is not locerr.Error, it's converted into locerr.Error. 287 | func NotefIn(start, end Pos, err error, format string, args ...interface{}) *Error { 288 | return NoteIn(start, end, err, fmt.Sprintf(format, args...)) 289 | } 290 | 291 | // NotefAt adds positional information and stack additional formatted message to the original error If given error is not locerr.Error, it's converted into locerr.Error. 292 | func NotefAt(pos Pos, err error, format string, args ...interface{}) *Error { 293 | return NoteIn(pos, Pos{}, err, fmt.Sprintf(format, args...)) 294 | } 295 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | func testCalcPos(src *Source, offset int) Pos { 12 | code := src.Code 13 | o, l, c, end := 0, 1, 1, len(code) 14 | for o != end { 15 | if o == offset { 16 | return Pos{o, l, c, src} 17 | } 18 | if code[o] == '\n' { 19 | l++ 20 | c = 1 21 | } else { 22 | c++ 23 | } 24 | o++ 25 | } 26 | if o != offset { 27 | panic("Offsetis illegal") 28 | } 29 | return Pos{o, l, c, src} 30 | } 31 | 32 | func TestFunctionsAndMethods(t *testing.T) { 33 | src := NewDummySource( 34 | `int main() { 35 | foo(aaa, 36 | bbb, 37 | ccc); 38 | return 0; 39 | }`, 40 | ) 41 | 42 | s := Pos{21, 2, 9, src} 43 | e := Pos{50, 4, 11, src} 44 | 45 | snip := ` 46 | 47 | > foo(aaa, 48 | > bbb, 49 | > ccc); 50 | ` 51 | oneline := ` 52 | 53 | > foo(aaa, 54 | ` 55 | loc := " (at :2:9)" 56 | 57 | cases := []struct { 58 | what string 59 | err *Error 60 | want string 61 | }{ 62 | { 63 | what: "NewError", 64 | err: NewError("This is error text"), 65 | want: "Error: This is error text", 66 | }, 67 | { 68 | what: "Errorf", 69 | err: Errorf("This is error text: %d", 42), 70 | want: "Error: This is error text: 42", 71 | }, 72 | { 73 | what: "ErrorIn", 74 | err: ErrorIn(s, e, "This is error text"), 75 | want: "Error: This is error text" + loc + snip, 76 | }, 77 | { 78 | what: "ErrorfIn", 79 | err: ErrorfIn(s, e, "This is error text: %d", 42), 80 | want: "Error: This is error text: 42" + loc + snip, 81 | }, 82 | { 83 | what: "ErrorAt", 84 | err: ErrorAt(s, "This is error text"), 85 | want: "Error: This is error text" + loc + oneline, 86 | }, 87 | { 88 | what: "ErrorfAt", 89 | err: ErrorfAt(s, "This is error text: %d", 42), 90 | want: "Error: This is error text: 42" + loc + oneline, 91 | }, 92 | { 93 | what: "WithRange", 94 | err: WithRange(s, e, fmt.Errorf("This is error text")), 95 | want: "Error: This is error text" + loc + snip, 96 | }, 97 | { 98 | what: "WithPos", 99 | err: WithPos(s, fmt.Errorf("This is error text")), 100 | want: "Error: This is error text" + loc + oneline, 101 | }, 102 | { 103 | what: "Note to error", 104 | err: Note(fmt.Errorf("This is error text"), "This is note"), 105 | want: "Error: This is error text\n Note: This is note", 106 | }, 107 | { 108 | what: "Notef to error", 109 | err: Notef(fmt.Errorf("This is error text"), "This is note: %d", 42), 110 | want: "Error: This is error text\n Note: This is note: 42", 111 | }, 112 | { 113 | what: "Note to locerr.Error", 114 | err: Note(ErrorIn(s, e, "This is error text"), "This is note"), 115 | want: "Error: This is error text" + loc + "\n Note: This is note" + snip, 116 | }, 117 | { 118 | what: "Notef to locerr.Error", 119 | err: Notef(ErrorIn(s, e, "This is error text"), "This is note: %d", 42), 120 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + snip, 121 | }, 122 | { 123 | what: "NoteIn to error", 124 | err: NoteIn(s, e, fmt.Errorf("This is error text"), "This is note"), 125 | want: "Error: This is error text" + loc + "\n Note: This is note" + snip, 126 | }, 127 | { 128 | what: "NotefIn to error", 129 | err: NotefIn(s, e, fmt.Errorf("This is error text"), "This is note: %d", 42), 130 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + snip, 131 | }, 132 | { 133 | what: "NoteIn to locerr.Error", 134 | err: NoteIn(s, e, ErrorIn(s, e, "This is error text"), "This is note"), 135 | want: "Error: This is error text" + loc + "\n Note: This is note" + loc + snip, 136 | }, 137 | { 138 | what: "NotefIn to locerr.Error", 139 | err: NotefIn(s, e, ErrorIn(s, e, "This is error text"), "This is note: %d", 42), 140 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + loc + snip, 141 | }, 142 | { 143 | what: "NoteAt to error", 144 | err: NoteAt(s, fmt.Errorf("This is error text"), "This is note"), 145 | want: "Error: This is error text" + loc + "\n Note: This is note" + oneline, 146 | }, 147 | { 148 | what: "NotefAt to error", 149 | err: NotefAt(s, fmt.Errorf("This is error text"), "This is note: %d", 42), 150 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + oneline, 151 | }, 152 | { 153 | what: "NoteAt to locerr.Error", 154 | err: NoteAt(s, ErrorIn(s, e, "This is error text"), "This is note"), 155 | want: "Error: This is error text" + loc + "\n Note: This is note" + loc + snip, 156 | }, 157 | { 158 | what: "NotefAt to locerr.Error", 159 | err: NotefAt(s, ErrorIn(s, e, "This is error text"), "This is note: %d", 42), 160 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + loc + snip, 161 | }, 162 | { 163 | what: "Note method", 164 | err: ErrorIn(s, e, "This is error text").Note("This is note"), 165 | want: "Error: This is error text" + loc + "\n Note: This is note" + snip, 166 | }, 167 | { 168 | what: "Notef method", 169 | err: ErrorIn(s, e, "This is error text").Notef("This is note: %d", 42), 170 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + snip, 171 | }, 172 | { 173 | what: "NoteAt method", 174 | err: ErrorIn(s, e, "This is error text").NoteAt(s, "This is note"), 175 | want: "Error: This is error text" + loc + "\n Note: This is note" + loc + snip, 176 | }, 177 | { 178 | what: "NotefAt method", 179 | err: ErrorIn(s, e, "This is error text").NotefAt(s, "This is note: %d", 42), 180 | want: "Error: This is error text" + loc + "\n Note: This is note: 42" + loc + snip, 181 | }, 182 | { 183 | what: "nested notes", 184 | err: Note(ErrorIn(s, e, "This is error text"), "This is note").NoteAt(s, "This is note second"), 185 | want: "Error: This is error text" + loc + "\n Note: This is note\n Note: This is note second" + loc + snip, 186 | }, 187 | { 188 | what: "set range later", 189 | err: NewError("This is error text").In(s, e), 190 | want: "Error: This is error text" + loc + snip, 191 | }, 192 | { 193 | what: "set pos later", 194 | err: NewError("This is error text").At(s), 195 | want: "Error: This is error text" + loc + oneline, 196 | }, 197 | { 198 | what: "overwrite range with pos", 199 | err: NewError("This is error text").In(s, e).At(s), 200 | want: "Error: This is error text" + loc + oneline, 201 | }, 202 | } 203 | 204 | for _, tc := range cases { 205 | t.Run(tc.what, func(t *testing.T) { 206 | have := tc.err.Error() 207 | if have != tc.want { 208 | t.Fatalf("Unexpected error message.\nwant:\n'%s'\nhave:\n'%s'", tc.want, have) 209 | } 210 | }) 211 | } 212 | } 213 | 214 | func TestCodeSnippet(t *testing.T) { 215 | cases := []struct { 216 | what string 217 | code string 218 | from int 219 | to int 220 | want []string 221 | }{ 222 | { 223 | what: "whole in a line", 224 | code: "abc", 225 | from: 0, 226 | to: 2, 227 | want: []string{ 228 | "> abc", 229 | }, 230 | }, 231 | { 232 | what: "slice in a line", 233 | code: "abc", 234 | from: 1, 235 | to: 2, 236 | want: []string{ 237 | "> abc", 238 | }, 239 | }, 240 | { 241 | what: "slice in a line with indent", 242 | code: " abc", 243 | from: 3, 244 | to: 4, 245 | want: []string{ 246 | "> abc", 247 | }, 248 | }, 249 | { 250 | what: "only white spaces", 251 | code: " ", 252 | from: 3, 253 | to: 4, 254 | want: []string{ 255 | "> ", 256 | }, 257 | }, 258 | { 259 | what: "whole two lines", 260 | code: "aaa\nbbb", 261 | from: 0, 262 | to: 7, 263 | want: []string{ 264 | "> aaa", 265 | "> bbb", 266 | }, 267 | }, 268 | { 269 | what: "partial two lines", 270 | code: "aaa\nbbb", 271 | from: 2, 272 | to: 5, 273 | want: []string{ 274 | "> aaa", 275 | "> bbb", 276 | }, 277 | }, 278 | { 279 | what: "indented two lines", 280 | code: " aaa\n bbb", 281 | from: 2, 282 | to: 8, 283 | want: []string{ 284 | "> aaa", 285 | "> bbb", 286 | }, 287 | }, 288 | { 289 | what: "start on newline", 290 | code: "aaa\nbbb", 291 | from: 3, 292 | to: 7, 293 | want: []string{ 294 | "> aaa", 295 | "> bbb", 296 | }, 297 | }, 298 | { 299 | what: "start just after newline", 300 | code: "aaa\nbbb", 301 | from: 4, 302 | to: 7, 303 | want: []string{ 304 | "> bbb", 305 | }, 306 | }, 307 | { 308 | what: "end just before newline", 309 | code: "aaa\nbbb", 310 | from: 1, 311 | to: 2, 312 | want: []string{ 313 | "> aaa", 314 | }, 315 | }, 316 | { 317 | what: "end on newline", 318 | code: "aaa\nbbb", 319 | from: 1, 320 | to: 3, 321 | want: []string{ 322 | "> aaa", 323 | }, 324 | }, 325 | { 326 | what: "end just after newline", 327 | code: "aaa\nbbb", 328 | from: 1, 329 | to: 4, 330 | want: []string{ 331 | "> aaa", 332 | "> bbb", 333 | }, 334 | }, 335 | { 336 | what: "whole multi lines", 337 | code: "aaa\nbbb\nccc\nddd\neee", 338 | from: 0, 339 | to: 19, 340 | want: []string{ 341 | "> aaa", 342 | "> bbb", 343 | "> ccc", 344 | "> ddd", 345 | "> eee", 346 | }, 347 | }, 348 | { 349 | what: "whole multi indented lines", 350 | code: "\t aaa\n\t\tbbb\n ccc\n \tddd\neee", 351 | from: 0, 352 | to: 29, 353 | want: []string{ 354 | "> aaa", 355 | "> bbb", 356 | "> ccc", 357 | "> ddd", 358 | "> eee", 359 | }, 360 | }, 361 | { 362 | what: "part of multi lines", 363 | code: "aaa\nbbb\nccc\nddd\neee", 364 | from: 5, 365 | to: 14, 366 | want: []string{ 367 | "> bbb", 368 | "> ccc", 369 | "> ddd", 370 | }, 371 | }, 372 | { 373 | what: "containing empty lines", 374 | code: "aaa\n\n\nccc\n\neee", 375 | from: 2, 376 | to: 13, 377 | want: []string{ 378 | "> aaa", 379 | "> ", 380 | "> ", 381 | "> ccc", 382 | "> ", 383 | "> eee", 384 | }, 385 | }, 386 | { 387 | what: "containing only whitespaces lines", 388 | code: "aaa\n \n\t\nccc\n\neee", 389 | from: 2, 390 | to: 17, 391 | want: []string{ 392 | "> aaa", 393 | "> ", 394 | "> ", 395 | "> ccc", 396 | "> ", 397 | "> eee", 398 | }, 399 | }, 400 | } 401 | 402 | for _, tc := range cases { 403 | t.Run(tc.what, func(t *testing.T) { 404 | src := NewDummySource(tc.code) 405 | err := ErrorIn(testCalcPos(src, tc.from), testCalcPos(src, tc.to), "text") 406 | have := strings.SplitN(err.Error(), "\n", 3)[2] 407 | want := strings.Join(tc.want, "\n") + "\n" 408 | if have != want { 409 | t.Fatalf("Unexpected snippet\n\nwant:\n'%s'\nhave:\n'%s'", want, have) 410 | } 411 | }) 412 | } 413 | } 414 | 415 | func TestOnelineSnip(t *testing.T) { 416 | cases := []struct { 417 | what string 418 | code string 419 | line int 420 | want string 421 | }{ 422 | { 423 | what: "first line", 424 | code: "aaa\nbbb\nccc", 425 | line: 1, 426 | want: "> aaa", 427 | }, 428 | { 429 | what: "second line", 430 | code: "aaa\nbbb\nccc", 431 | line: 2, 432 | want: "> bbb", 433 | }, 434 | { 435 | what: "last line", 436 | code: "aaa\nbbb\nccc", 437 | line: 3, 438 | want: "> ccc", 439 | }, 440 | { 441 | what: "empty line", 442 | code: "aaa\n\nccc", 443 | line: 2, 444 | want: "", 445 | }, 446 | { 447 | what: "out of range", 448 | code: "aaa\naaa\nbbb", 449 | line: 4, 450 | want: "", 451 | }, 452 | { 453 | what: "empty line at last", 454 | code: "aaa\naaa\n", 455 | line: 3, 456 | want: "", 457 | }, 458 | } 459 | for _, tc := range cases { 460 | t.Run(tc.what, func(t *testing.T) { 461 | src := NewDummySource(tc.code) 462 | // Only pos.Line is referred 463 | err := ErrorAt(Pos{0, tc.line, 0, src}, "text") 464 | if tc.want == "" { 465 | have := err.Error() 466 | lines := strings.Split(have, "\n") 467 | if len(lines) != 1 || !strings.HasPrefix(lines[0], "Error: text (at :") { 468 | t.Fatal("Oneline snppet should be skipped but got:", have) 469 | } 470 | } else { 471 | have := strings.Split(err.Error(), "\n")[2] 472 | if have != tc.want { 473 | t.Fatalf("Unexpected snippet\n\nwant:'%s'\nhave:'%s'", tc.want, have) 474 | } 475 | } 476 | }) 477 | } 478 | } 479 | 480 | func TestCodeIsEmpty(t *testing.T) { 481 | s := NewDummySource("") 482 | p := Pos{0, 1, 1, s} 483 | err := ErrorIn(p, p, "This is error text") 484 | want := "Error: This is error text (at :1:1)" 485 | got := err.Error() 486 | 487 | if want != got { 488 | t.Fatalf("Unexpected error message. want: '%s', got: '%s'", want, got) 489 | } 490 | } 491 | 492 | // Fallback into oneline snippet 493 | func TestSnipIsEmpty(t *testing.T) { 494 | s := NewDummySource("abc") 495 | p := Pos{1, 1, 2, s} 496 | err := ErrorIn(p, p, "This is error text") 497 | want := `Error: This is error text (at :1:2) 498 | 499 | > abc 500 | ` 501 | got := err.Error() 502 | 503 | if want != got { 504 | t.Fatalf("Unexpected error message. want: '%s', got: '%s'", want, got) 505 | } 506 | } 507 | 508 | func TestSetColor(t *testing.T) { 509 | defer func() { SetColor(true) }() 510 | SetColor(false) 511 | if !color.NoColor { 512 | t.Fatal("Color should be disabled") 513 | } 514 | SetColor(true) 515 | if color.NoColor { 516 | t.Fatal("Color should be enabled") 517 | } 518 | SetColor(false) 519 | if !color.NoColor { 520 | t.Fatal("Color should be disabled (2)") 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | const code = `function foo(x: bool): int { 9 | return (if x then 42 else 21) 10 | } 11 | 12 | function main() { 13 | foo(true, 14 | 42, 15 | "test") 16 | }` 17 | 18 | func ExampleErrorWithRange() { 19 | // At first you should gain entire source as *Source instance. 20 | 21 | src := NewDummySource(code) 22 | 23 | // You can get *Source instance from file (NewSourceFromFile) or stdin (NewSourceFromStdin) also. 24 | 25 | // Let's say to find an error at some range in the source. 26 | 27 | start := Pos{ 28 | Offset: 88, 29 | Line: 6, 30 | Column: 7, 31 | File: src, 32 | } 33 | end := Pos{ 34 | Offset: 116, 35 | Line: 9, 36 | Column: 12, 37 | File: src, 38 | } 39 | 40 | // NewError or other factory functions make a new error instance with the range. Error instance implements 41 | // error interface so it can be handled like other error types. 42 | 43 | err := ErrorIn(start, end, "Calling 'foo' with wrong number of argument") 44 | 45 | // Assume that you find additional information (location of variable and its type). Then you can add some 46 | // notes to the error. Notes can be added by wrapping errors like pkg/errors library. 47 | 48 | prev := Pos{ 49 | Offset: 9, 50 | Line: 1, 51 | Column: 10, 52 | File: src, 53 | } 54 | 55 | err = err.NoteAt(prev, "Defined with 1 parameter") 56 | err = err.NoteAt(prev, "'foo' was defined as 'bool -> int'") 57 | 58 | // Finally you can see the result! 59 | 60 | // Get the error message as string. Note that this is only for non-Windows OS. 61 | fmt.Println(err) 62 | 63 | // Directly writes the error message into given file. 64 | // This supports Windows. Useful to output from stdout or stderr. 65 | err.PrintToFile(os.Stdout) 66 | } 67 | 68 | func ExampleErrorWithOnePos() { 69 | src := NewDummySource(code) 70 | 71 | pos := Pos{ 72 | Offset: 88, 73 | Line: 6, 74 | Column: 7, 75 | File: src, 76 | } 77 | 78 | // If you have only one position information rather than two, 'start' position and 'end' position, 79 | // ErrorAt() is available instead of ErrorIn() ErrorAt() takes one Pos instance. 80 | err := ErrorAt(pos, "Calling 'foo' with wrong number of argument") 81 | 82 | // In this case, line snippet is shown in error message. `pos.Line` is used to get line from source text. 83 | fmt.Println(err) 84 | } 85 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | This package is used for fuzzing tests using [go-fuzz](https://github.com/dvyukov/go-fuzz). Please read [README at root](../README.md) for more detail. 2 | -------------------------------------------------------------------------------- /fuzz/main.go: -------------------------------------------------------------------------------- 1 | package locerrfuzzing 2 | 3 | import ( 4 | "github.com/rhysd/locerr" 5 | "math/rand" 6 | "strings" 7 | ) 8 | 9 | func offsetPos(src *locerr.Source, offset int) locerr.Pos { 10 | o, l, c, end := 0, 1, 1, len(src.Code) 11 | for o != end { 12 | if o == offset { 13 | return locerr.Pos{o, l, c, src} 14 | } 15 | if src.Code[o] == '\n' { 16 | l++ 17 | c = 1 18 | } else { 19 | c++ 20 | } 21 | o++ 22 | } 23 | return locerr.Pos{o, l, c, src} 24 | } 25 | 26 | // Fuzz do fuzzing test using go-fuzz 27 | func Fuzz(data []byte) int { 28 | src := locerr.NewDummySource(string(data)) 29 | len := len(data) 30 | if len == 0 { 31 | p := locerr.Pos{0, 1, 1, src} 32 | return fuzz(src, p, p) 33 | } 34 | 35 | o := 0 36 | if len > 1 { 37 | o = rand.Intn(len - 1) 38 | } 39 | s := offsetPos(src, o) 40 | o = rand.Intn(len-o) + o 41 | e := offsetPos(src, o) 42 | return fuzz(src, s, e) 43 | } 44 | 45 | func fuzz(src *locerr.Source, start locerr.Pos, end locerr.Pos) int { 46 | err := locerr.ErrorIn(start, end, "fuzz") 47 | err = err.NoteAt(start, "note1") 48 | err = err.Note("note2") 49 | msg := err.Error() 50 | if !strings.Contains(msg, "fuzz") || !strings.Contains(msg, "note1") || !strings.Contains(msg, "note2") { 51 | panic("Unexpected error message: " + msg) 52 | } 53 | return 1 // data is good for fuzzing 54 | } 55 | -------------------------------------------------------------------------------- /position.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | var currentDir string 10 | 11 | func init() { 12 | currentDir, _ = filepath.Abs(filepath.Dir(os.Args[0])) 13 | } 14 | 15 | // Pos represents some point in a source code. 16 | type Pos struct { 17 | // Offset from the beginning of code. 18 | Offset int 19 | // Line number. 20 | Line int 21 | // Column number. 22 | Column int 23 | // File of this position. 24 | File *Source 25 | } 26 | 27 | // String makes a string representation of the position. Format is 'file:line:column'. 28 | func (p Pos) String() string { 29 | if p.File == nil { 30 | return ":0:0" 31 | } 32 | f := p.File.Path 33 | if p.File.Exists && currentDir != "" && filepath.HasPrefix(f, currentDir) { 34 | f, _ = filepath.Rel(currentDir, f) 35 | } 36 | return fmt.Sprintf("%s:%d:%d", f, p.Line, p.Column) 37 | } 38 | -------------------------------------------------------------------------------- /position_test.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | ) 7 | 8 | func TestStringizePos(t *testing.T) { 9 | src := NewDummySource("test") 10 | p := Pos{4, 1, 3, src} 11 | want := ":1:3" 12 | if p.String() != want { 13 | t.Fatal("Unknown position format: ", p.String(), "wanted", want) 14 | } 15 | } 16 | 17 | func TestStringizeUnknownFile(t *testing.T) { 18 | p := Pos{} 19 | want := ":0:0" 20 | if p.String() != want { 21 | t.Fatal("Unexpected position", p.String(), "wanted", want) 22 | } 23 | } 24 | 25 | func TestPosStringCanonicalPath(t *testing.T) { 26 | f, err := filepath.Abs("position_test.go") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | src, err := NewSourceFromFile("position_test.go") 32 | saved := currentDir 33 | currentDir = filepath.Dir(f) 34 | defer func() { currentDir = saved }() 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | have := Pos{0, 1, 1, src}.String() 39 | want := "position_test.go:1:1" // Prefix was stripped 40 | if have != want { 41 | t.Fatal(want, "was wanted but have", have) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // Source represents Dachs source code file. It may be a file on filesystem, stdin or dummy file. 11 | type Source struct { 12 | // Path of the file. if it is stdin. if it is a dummy source. 13 | Path string 14 | // Code contained in this source. 15 | Code []byte 16 | // Exists indicates this source exists in filesystem or not. 17 | Exists bool 18 | } 19 | 20 | // NewSourceFromFile make *Source object from file path. 21 | func NewSourceFromFile(file string) (*Source, error) { 22 | path, err := filepath.Abs(file) 23 | if err != nil { 24 | return nil, err 25 | } 26 | b, err := ioutil.ReadFile(path) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &Source{path, b, true}, nil 31 | } 32 | 33 | // NewSourceFromStdin make *Source object from stdin. User will need to input source code into stdin. 34 | func NewSourceFromStdin() (*Source, error) { 35 | b, err := ioutil.ReadAll(os.Stdin) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &Source{"", b, false}, nil 40 | } 41 | 42 | // NewDummySource make *Source with passed code. The source is actually does not exist in filesystem (so dummy). This is used for tests. 43 | func NewDummySource(code string) *Source { 44 | return &Source{"", []byte(code), false} 45 | } 46 | 47 | // BaseName makes a base name from the name of source. If the source does not exist in filesystem, its base name will be 'out'. 48 | func (src *Source) BaseName() string { 49 | if !src.Exists { 50 | return "out" 51 | } 52 | b := filepath.Base(src.Path) 53 | return strings.TrimSuffix(b, filepath.Ext(b)) 54 | } 55 | 56 | func (src *Source) String() string { 57 | return "source:" + src.Path 58 | } 59 | -------------------------------------------------------------------------------- /source_test.go: -------------------------------------------------------------------------------- 1 | package locerr 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestReadFromFile(t *testing.T) { 9 | s, err := NewSourceFromFile("./source.go") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | if !strings.HasSuffix(s.Path, "source.go") { 15 | t.Errorf("Unexpected file name %s", s.Path) 16 | } 17 | 18 | if s.Code == nil { 19 | t.Errorf("Code was not read properly") 20 | } 21 | 22 | if !s.Exists { 23 | t.Errorf("File must exist") 24 | } 25 | } 26 | 27 | func TestUnexistFile(t *testing.T) { 28 | _, err := NewSourceFromFile("./__unknown_file.ml") 29 | if err == nil { 30 | t.Fatalf("Unknown error must cause an error") 31 | } 32 | } 33 | 34 | func TestReadFromStdin(t *testing.T) { 35 | s, err := NewSourceFromStdin() 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if s.Path != "" { 41 | t.Errorf("Unexpected file name %s", s.Path) 42 | } 43 | 44 | if s.Code == nil { 45 | t.Errorf("Code was not read properly") 46 | } 47 | 48 | if s.Exists { 49 | t.Errorf("File must not exist") 50 | } 51 | } 52 | 53 | func TestBaseName(t *testing.T) { 54 | fromFile, err := NewSourceFromFile("./source.go") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | fromStdin, err := NewSourceFromStdin() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | fromDummy := NewDummySource("test") 63 | 64 | for _, tc := range []struct { 65 | expected string 66 | source *Source 67 | }{ 68 | {"source", fromFile}, 69 | {"out", fromStdin}, 70 | {"out", fromDummy}, 71 | } { 72 | actual := tc.source.BaseName() 73 | if tc.expected != actual { 74 | t.Errorf("Expected base name of '%s' to be '%s', but actually it was '%s'", tc.source.Path, tc.expected, actual) 75 | } 76 | } 77 | } 78 | 79 | func TestSourceString(t *testing.T) { 80 | s := NewDummySource("") 81 | if s.String() != "source:" { 82 | t.Fatal("Unknown source name:", s) 83 | } 84 | } 85 | --------------------------------------------------------------------------------