38b0630a2a
Change-Id: I94bca46d6d30da683cacb0f6ea1fdf26a5a46bc8
796 lines
16 KiB
Go
796 lines
16 KiB
Go
// Copyright 2014 Google Inc. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package parser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/scanner"
|
|
)
|
|
|
|
var errTooManyErrors = errors.New("too many errors")
|
|
|
|
const maxErrors = 1
|
|
|
|
type ParseError struct {
|
|
Err error
|
|
Pos scanner.Position
|
|
}
|
|
|
|
func (e *ParseError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Pos, e.Err)
|
|
}
|
|
|
|
type File struct {
|
|
Name string
|
|
Defs []Definition
|
|
Comments []Comment
|
|
}
|
|
|
|
func parse(p *parser) (file *File, errs []error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
if r == errTooManyErrors {
|
|
errs = p.errors
|
|
return
|
|
}
|
|
panic(r)
|
|
}
|
|
}()
|
|
|
|
defs := p.parseDefinitions()
|
|
p.accept(scanner.EOF)
|
|
errs = p.errors
|
|
comments := p.comments
|
|
|
|
return &File{
|
|
Name: p.scanner.Filename,
|
|
Defs: defs,
|
|
Comments: comments,
|
|
}, errs
|
|
|
|
}
|
|
|
|
func ParseAndEval(filename string, r io.Reader, scope *Scope) (file *File, errs []error) {
|
|
p := newParser(r, scope)
|
|
p.eval = true
|
|
p.scanner.Filename = filename
|
|
|
|
return parse(p)
|
|
}
|
|
|
|
func Parse(filename string, r io.Reader, scope *Scope) (file *File, errs []error) {
|
|
p := newParser(r, scope)
|
|
p.scanner.Filename = filename
|
|
|
|
return parse(p)
|
|
}
|
|
|
|
type parser struct {
|
|
scanner scanner.Scanner
|
|
tok rune
|
|
errors []error
|
|
scope *Scope
|
|
comments []Comment
|
|
eval bool
|
|
}
|
|
|
|
func newParser(r io.Reader, scope *Scope) *parser {
|
|
p := &parser{}
|
|
p.scope = scope
|
|
p.scanner.Init(r)
|
|
p.scanner.Error = func(sc *scanner.Scanner, msg string) {
|
|
p.errorf(msg)
|
|
}
|
|
p.scanner.Mode = scanner.ScanIdents | scanner.ScanStrings |
|
|
scanner.ScanRawStrings | scanner.ScanComments
|
|
p.next()
|
|
return p
|
|
}
|
|
|
|
func (p *parser) errorf(format string, args ...interface{}) {
|
|
pos := p.scanner.Position
|
|
if !pos.IsValid() {
|
|
pos = p.scanner.Pos()
|
|
}
|
|
err := &ParseError{
|
|
Err: fmt.Errorf(format, args...),
|
|
Pos: pos,
|
|
}
|
|
p.errors = append(p.errors, err)
|
|
if len(p.errors) >= maxErrors {
|
|
panic(errTooManyErrors)
|
|
}
|
|
}
|
|
|
|
func (p *parser) accept(toks ...rune) bool {
|
|
for _, tok := range toks {
|
|
if p.tok != tok {
|
|
p.errorf("expected %s, found %s", scanner.TokenString(tok),
|
|
scanner.TokenString(p.tok))
|
|
return false
|
|
}
|
|
p.next()
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *parser) next() {
|
|
if p.tok != scanner.EOF {
|
|
p.tok = p.scanner.Scan()
|
|
for p.tok == scanner.Comment {
|
|
lines := strings.Split(p.scanner.TokenText(), "\n")
|
|
p.comments = append(p.comments, Comment{lines, p.scanner.Position})
|
|
p.tok = p.scanner.Scan()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseDefinitions() (defs []Definition) {
|
|
for {
|
|
switch p.tok {
|
|
case scanner.Ident:
|
|
ident := p.scanner.TokenText()
|
|
pos := p.scanner.Position
|
|
|
|
p.accept(scanner.Ident)
|
|
|
|
switch p.tok {
|
|
case '+':
|
|
p.accept('+')
|
|
defs = append(defs, p.parseAssignment(ident, pos, "+="))
|
|
case '=':
|
|
defs = append(defs, p.parseAssignment(ident, pos, "="))
|
|
case '{', '(':
|
|
defs = append(defs, p.parseModule(ident, pos))
|
|
default:
|
|
p.errorf("expected \"=\" or \"+=\" or \"{\" or \"(\", found %s",
|
|
scanner.TokenString(p.tok))
|
|
}
|
|
case scanner.EOF:
|
|
return
|
|
default:
|
|
p.errorf("expected assignment or module definition, found %s",
|
|
scanner.TokenString(p.tok))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseAssignment(name string,
|
|
namePos scanner.Position, assigner string) (assignment *Assignment) {
|
|
|
|
assignment = new(Assignment)
|
|
|
|
pos := p.scanner.Position
|
|
if !p.accept('=') {
|
|
return
|
|
}
|
|
value := p.parseExpression()
|
|
|
|
assignment.Name = Ident{name, namePos}
|
|
assignment.Value = value
|
|
assignment.OrigValue = value
|
|
assignment.Pos = pos
|
|
assignment.Assigner = assigner
|
|
|
|
if p.scope != nil {
|
|
if assigner == "+=" {
|
|
if old, err := p.scope.Get(assignment.Name.Name); err == nil {
|
|
if old.Referenced {
|
|
p.errorf("modified variable with += after referencing")
|
|
}
|
|
old.Value, err = p.evaluateOperator(old.Value, assignment.Value, '+', assignment.Pos)
|
|
return
|
|
}
|
|
}
|
|
|
|
err := p.scope.Add(assignment)
|
|
if err != nil {
|
|
p.errorf("%s", err.Error())
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseModule(typ string,
|
|
typPos scanner.Position) (module *Module) {
|
|
|
|
module = new(Module)
|
|
compat := false
|
|
lbracePos := p.scanner.Position
|
|
if p.tok == '{' {
|
|
compat = true
|
|
}
|
|
|
|
if !p.accept(p.tok) {
|
|
return
|
|
}
|
|
properties := p.parsePropertyList(true, compat)
|
|
rbracePos := p.scanner.Position
|
|
if !compat {
|
|
p.accept(')')
|
|
} else {
|
|
p.accept('}')
|
|
}
|
|
|
|
module.Type = Ident{typ, typPos}
|
|
module.Properties = properties
|
|
module.LbracePos = lbracePos
|
|
module.RbracePos = rbracePos
|
|
return
|
|
}
|
|
|
|
func (p *parser) parsePropertyList(isModule, compat bool) (properties []*Property) {
|
|
for p.tok == scanner.Ident {
|
|
property := p.parseProperty(isModule, compat)
|
|
properties = append(properties, property)
|
|
|
|
if p.tok != ',' {
|
|
// There was no comma, so the list is done.
|
|
break
|
|
}
|
|
|
|
p.accept(',')
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseProperty(isModule, compat bool) (property *Property) {
|
|
property = new(Property)
|
|
|
|
name := p.scanner.TokenText()
|
|
namePos := p.scanner.Position
|
|
p.accept(scanner.Ident)
|
|
pos := p.scanner.Position
|
|
|
|
if isModule {
|
|
if compat && p.tok == ':' {
|
|
p.accept(':')
|
|
} else {
|
|
if !p.accept('=') {
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
if !p.accept(':') {
|
|
return
|
|
}
|
|
}
|
|
|
|
value := p.parseExpression()
|
|
|
|
property.Name = Ident{name, namePos}
|
|
property.Value = value
|
|
property.Pos = pos
|
|
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseExpression() (value Value) {
|
|
value = p.parseValue()
|
|
switch p.tok {
|
|
case '+':
|
|
return p.parseOperator(value)
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
func (p *parser) evaluateOperator(value1, value2 Value, operator rune,
|
|
pos scanner.Position) (Value, error) {
|
|
|
|
value := Value{}
|
|
|
|
if p.eval {
|
|
if value1.Type != value2.Type {
|
|
return Value{}, fmt.Errorf("mismatched type in operator %c: %s != %s", operator,
|
|
value1.Type, value2.Type)
|
|
}
|
|
|
|
value = value1
|
|
value.Variable = ""
|
|
|
|
switch operator {
|
|
case '+':
|
|
switch value1.Type {
|
|
case String:
|
|
value.StringValue = value1.StringValue + value2.StringValue
|
|
case List:
|
|
value.ListValue = append([]Value{}, value1.ListValue...)
|
|
value.ListValue = append(value.ListValue, value2.ListValue...)
|
|
case Map:
|
|
var err error
|
|
value.MapValue, err = p.addMaps(value.MapValue, value2.MapValue, pos)
|
|
if err != nil {
|
|
return Value{}, err
|
|
}
|
|
default:
|
|
return Value{}, fmt.Errorf("operator %c not supported on type %s", operator,
|
|
value1.Type)
|
|
}
|
|
default:
|
|
panic("unknown operator " + string(operator))
|
|
}
|
|
}
|
|
|
|
value.Expression = &Expression{
|
|
Args: [2]Value{value1, value2},
|
|
Operator: operator,
|
|
Pos: pos,
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
func (p *parser) addMaps(map1, map2 []*Property, pos scanner.Position) ([]*Property, error) {
|
|
ret := make([]*Property, 0, len(map1))
|
|
|
|
inMap1 := make(map[string]*Property)
|
|
inMap2 := make(map[string]*Property)
|
|
inBoth := make(map[string]*Property)
|
|
|
|
for _, prop1 := range map1 {
|
|
inMap1[prop1.Name.Name] = prop1
|
|
}
|
|
|
|
for _, prop2 := range map2 {
|
|
inMap2[prop2.Name.Name] = prop2
|
|
if _, ok := inMap1[prop2.Name.Name]; ok {
|
|
inBoth[prop2.Name.Name] = prop2
|
|
}
|
|
}
|
|
|
|
for _, prop1 := range map1 {
|
|
if prop2, ok := inBoth[prop1.Name.Name]; ok {
|
|
var err error
|
|
newProp := *prop1
|
|
newProp.Value, err = p.evaluateOperator(prop1.Value, prop2.Value, '+', pos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret = append(ret, &newProp)
|
|
} else {
|
|
ret = append(ret, prop1)
|
|
}
|
|
}
|
|
|
|
for _, prop2 := range map2 {
|
|
if _, ok := inBoth[prop2.Name.Name]; !ok {
|
|
ret = append(ret, prop2)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (p *parser) parseOperator(value1 Value) Value {
|
|
operator := p.tok
|
|
pos := p.scanner.Position
|
|
p.accept(operator)
|
|
|
|
value2 := p.parseExpression()
|
|
|
|
value, err := p.evaluateOperator(value1, value2, operator, pos)
|
|
if err != nil {
|
|
p.errorf(err.Error())
|
|
return Value{}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func (p *parser) parseValue() (value Value) {
|
|
switch p.tok {
|
|
case scanner.Ident:
|
|
return p.parseVariable()
|
|
case scanner.String:
|
|
return p.parseStringValue()
|
|
case '[':
|
|
return p.parseListValue()
|
|
case '{':
|
|
return p.parseMapValue()
|
|
default:
|
|
p.errorf("expected bool, list, or string value; found %s",
|
|
scanner.TokenString(p.tok))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseVariable() (value Value) {
|
|
switch text := p.scanner.TokenText(); text {
|
|
case "true":
|
|
value.Type = Bool
|
|
value.BoolValue = true
|
|
case "false":
|
|
value.Type = Bool
|
|
value.BoolValue = false
|
|
default:
|
|
variable := p.scanner.TokenText()
|
|
if p.eval {
|
|
assignment, err := p.scope.Get(variable)
|
|
if err != nil {
|
|
p.errorf(err.Error())
|
|
}
|
|
assignment.Referenced = true
|
|
value = assignment.Value
|
|
}
|
|
value.Variable = variable
|
|
}
|
|
value.Pos = p.scanner.Position
|
|
|
|
p.accept(scanner.Ident)
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseStringValue() (value Value) {
|
|
value.Type = String
|
|
value.Pos = p.scanner.Position
|
|
str, err := strconv.Unquote(p.scanner.TokenText())
|
|
if err != nil {
|
|
p.errorf("couldn't parse string: %s", err)
|
|
return
|
|
}
|
|
value.StringValue = str
|
|
p.accept(scanner.String)
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseListValue() (value Value) {
|
|
value.Type = List
|
|
value.Pos = p.scanner.Position
|
|
if !p.accept('[') {
|
|
return
|
|
}
|
|
|
|
var elements []Value
|
|
for p.tok != ']' {
|
|
element := p.parseExpression()
|
|
if p.eval && element.Type != String {
|
|
p.errorf("Expected string in list, found %s", element.String())
|
|
return
|
|
}
|
|
elements = append(elements, element)
|
|
|
|
if p.tok != ',' {
|
|
// There was no comma, so the list is done.
|
|
break
|
|
}
|
|
|
|
p.accept(',')
|
|
}
|
|
|
|
value.ListValue = elements
|
|
value.EndPos = p.scanner.Position
|
|
|
|
p.accept(']')
|
|
return
|
|
}
|
|
|
|
func (p *parser) parseMapValue() (value Value) {
|
|
value.Type = Map
|
|
value.Pos = p.scanner.Position
|
|
if !p.accept('{') {
|
|
return
|
|
}
|
|
|
|
properties := p.parsePropertyList(false, false)
|
|
value.MapValue = properties
|
|
|
|
value.EndPos = p.scanner.Position
|
|
p.accept('}')
|
|
return
|
|
}
|
|
|
|
type Expression struct {
|
|
Args [2]Value
|
|
Operator rune
|
|
Pos scanner.Position
|
|
}
|
|
|
|
func (e *Expression) Copy() *Expression {
|
|
ret := *e
|
|
ret.Args[0] = e.Args[0].Copy()
|
|
ret.Args[1] = e.Args[1].Copy()
|
|
return &ret
|
|
}
|
|
|
|
func (e *Expression) String() string {
|
|
return fmt.Sprintf("(%s %c %s)@%d:%s", e.Args[0].String(), e.Operator, e.Args[1].String(),
|
|
e.Pos.Offset, e.Pos)
|
|
}
|
|
|
|
type ValueType int
|
|
|
|
const (
|
|
Bool ValueType = iota
|
|
String
|
|
List
|
|
Map
|
|
)
|
|
|
|
func (p ValueType) String() string {
|
|
switch p {
|
|
case Bool:
|
|
return "bool"
|
|
case String:
|
|
return "string"
|
|
case List:
|
|
return "list"
|
|
case Map:
|
|
return "map"
|
|
default:
|
|
panic(fmt.Errorf("unknown value type: %d", p))
|
|
}
|
|
}
|
|
|
|
type Definition interface {
|
|
String() string
|
|
definitionTag()
|
|
}
|
|
|
|
type Assignment struct {
|
|
Name Ident
|
|
Value Value
|
|
OrigValue Value
|
|
Pos scanner.Position
|
|
Assigner string
|
|
Referenced bool
|
|
}
|
|
|
|
func (a *Assignment) String() string {
|
|
return fmt.Sprintf("%s@%d:%s %s %s", a.Name, a.Pos.Offset, a.Pos, a.Assigner, a.Value)
|
|
}
|
|
|
|
func (a *Assignment) definitionTag() {}
|
|
|
|
type Module struct {
|
|
Type Ident
|
|
Properties []*Property
|
|
LbracePos scanner.Position
|
|
RbracePos scanner.Position
|
|
}
|
|
|
|
func (m *Module) Copy() *Module {
|
|
ret := *m
|
|
ret.Properties = make([]*Property, len(m.Properties))
|
|
for i := range m.Properties {
|
|
ret.Properties[i] = m.Properties[i].Copy()
|
|
}
|
|
return &ret
|
|
}
|
|
|
|
func (m *Module) String() string {
|
|
propertyStrings := make([]string, len(m.Properties))
|
|
for i, property := range m.Properties {
|
|
propertyStrings[i] = property.String()
|
|
}
|
|
return fmt.Sprintf("%s@%d:%s-%d:%s{%s}", m.Type,
|
|
m.LbracePos.Offset, m.LbracePos,
|
|
m.RbracePos.Offset, m.RbracePos,
|
|
strings.Join(propertyStrings, ", "))
|
|
}
|
|
|
|
func (m *Module) definitionTag() {}
|
|
|
|
type Property struct {
|
|
Name Ident
|
|
Value Value
|
|
Pos scanner.Position
|
|
}
|
|
|
|
func (p *Property) Copy() *Property {
|
|
ret := *p
|
|
ret.Value = p.Value.Copy()
|
|
return &ret
|
|
}
|
|
|
|
func (p *Property) String() string {
|
|
return fmt.Sprintf("%s@%d:%s: %s", p.Name, p.Pos.Offset, p.Pos, p.Value)
|
|
}
|
|
|
|
type Ident struct {
|
|
Name string
|
|
Pos scanner.Position
|
|
}
|
|
|
|
func (i Ident) String() string {
|
|
return fmt.Sprintf("%s@%d:%s", i.Name, i.Pos.Offset, i.Pos)
|
|
}
|
|
|
|
type Value struct {
|
|
Type ValueType
|
|
BoolValue bool
|
|
StringValue string
|
|
ListValue []Value
|
|
MapValue []*Property
|
|
Expression *Expression
|
|
Variable string
|
|
Pos scanner.Position
|
|
EndPos scanner.Position
|
|
}
|
|
|
|
func (p Value) Copy() Value {
|
|
ret := p
|
|
if p.MapValue != nil {
|
|
ret.MapValue = make([]*Property, len(p.MapValue))
|
|
for i := range p.MapValue {
|
|
ret.MapValue[i] = p.MapValue[i].Copy()
|
|
}
|
|
}
|
|
if p.ListValue != nil {
|
|
ret.ListValue = make([]Value, len(p.ListValue))
|
|
for i := range p.ListValue {
|
|
ret.ListValue[i] = p.ListValue[i].Copy()
|
|
}
|
|
}
|
|
if p.Expression != nil {
|
|
ret.Expression = p.Expression.Copy()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (p Value) String() string {
|
|
var s string
|
|
if p.Variable != "" {
|
|
s += p.Variable + " = "
|
|
}
|
|
if p.Expression != nil {
|
|
s += p.Expression.String()
|
|
}
|
|
switch p.Type {
|
|
case Bool:
|
|
s += fmt.Sprintf("%t@%d:%s", p.BoolValue, p.Pos.Offset, p.Pos)
|
|
case String:
|
|
s += fmt.Sprintf("%q@%d:%s", p.StringValue, p.Pos.Offset, p.Pos)
|
|
case List:
|
|
valueStrings := make([]string, len(p.ListValue))
|
|
for i, value := range p.ListValue {
|
|
valueStrings[i] = value.String()
|
|
}
|
|
s += fmt.Sprintf("@%d:%s-%d:%s[%s]", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
|
|
strings.Join(valueStrings, ", "))
|
|
case Map:
|
|
propertyStrings := make([]string, len(p.MapValue))
|
|
for i, property := range p.MapValue {
|
|
propertyStrings[i] = property.String()
|
|
}
|
|
s += fmt.Sprintf("@%d:%s-%d:%s{%s}", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
|
|
strings.Join(propertyStrings, ", "))
|
|
default:
|
|
panic(fmt.Errorf("bad property type: %d", p.Type))
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
type Scope struct {
|
|
vars map[string]*Assignment
|
|
}
|
|
|
|
func NewScope(s *Scope) *Scope {
|
|
newScope := &Scope{
|
|
vars: make(map[string]*Assignment),
|
|
}
|
|
|
|
if s != nil {
|
|
for k, v := range s.vars {
|
|
newScope.vars[k] = v
|
|
}
|
|
}
|
|
|
|
return newScope
|
|
}
|
|
|
|
func (s *Scope) Add(assignment *Assignment) error {
|
|
if old, ok := s.vars[assignment.Name.Name]; ok {
|
|
return fmt.Errorf("variable already set, previous assignment: %s", old)
|
|
}
|
|
|
|
s.vars[assignment.Name.Name] = assignment
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Scope) Remove(name string) {
|
|
delete(s.vars, name)
|
|
}
|
|
|
|
func (s *Scope) Get(name string) (*Assignment, error) {
|
|
if a, ok := s.vars[name]; ok {
|
|
return a, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("variable %s not set", name)
|
|
}
|
|
|
|
func (s *Scope) String() string {
|
|
vars := []string{}
|
|
|
|
for k := range s.vars {
|
|
vars = append(vars, k)
|
|
}
|
|
|
|
sort.Strings(vars)
|
|
|
|
ret := []string{}
|
|
for _, v := range vars {
|
|
ret = append(ret, s.vars[v].String())
|
|
}
|
|
|
|
return strings.Join(ret, "\n")
|
|
}
|
|
|
|
type Comment struct {
|
|
Comment []string
|
|
Pos scanner.Position
|
|
}
|
|
|
|
func (c Comment) String() string {
|
|
l := 0
|
|
for _, comment := range c.Comment {
|
|
l += len(comment) + 1
|
|
}
|
|
buf := make([]byte, 0, l)
|
|
for _, comment := range c.Comment {
|
|
buf = append(buf, comment...)
|
|
buf = append(buf, '\n')
|
|
}
|
|
|
|
return string(buf)
|
|
}
|
|
|
|
// Return the text of the comment with // or /* and */ stripped
|
|
func (c Comment) Text() string {
|
|
l := 0
|
|
for _, comment := range c.Comment {
|
|
l += len(comment) + 1
|
|
}
|
|
buf := make([]byte, 0, l)
|
|
|
|
blockComment := false
|
|
if strings.HasPrefix(c.Comment[0], "/*") {
|
|
blockComment = true
|
|
}
|
|
|
|
for i, comment := range c.Comment {
|
|
if blockComment {
|
|
if i == 0 {
|
|
comment = strings.TrimPrefix(comment, "/*")
|
|
}
|
|
if i == len(c.Comment)-1 {
|
|
comment = strings.TrimSuffix(comment, "*/")
|
|
}
|
|
} else {
|
|
comment = strings.TrimPrefix(comment, "//")
|
|
}
|
|
buf = append(buf, comment...)
|
|
buf = append(buf, '\n')
|
|
}
|
|
|
|
return string(buf)
|
|
}
|
|
|
|
// Return the line number that the comment ends on
|
|
func (c Comment) EndLine() int {
|
|
return c.Pos.Line + len(c.Comment) - 1
|
|
}
|