|
|
@@ -11,15 +11,17 @@ import (
|
|
|
|
|
|
// ERParser implements ER diagram parsing following erDiagram.jison
|
|
|
type ERParser struct {
|
|
|
- tokens []lexer.Token
|
|
|
- current int
|
|
|
- diagram *ast.ERDiagram
|
|
|
+ tokens []lexer.Token
|
|
|
+ current int
|
|
|
+ diagram *ast.ERDiagram
|
|
|
+ entityMap map[string]*ast.EREntity // Keep track of entities by name for quick lookup
|
|
|
}
|
|
|
|
|
|
// NewERParser creates a new ER parser
|
|
|
func NewERParser() *ERParser {
|
|
|
return &ERParser{
|
|
|
- diagram: ast.NewERDiagram(),
|
|
|
+ diagram: ast.NewERDiagram(),
|
|
|
+ entityMap: make(map[string]*ast.EREntity),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -74,128 +76,133 @@ func (p *ERParser) parseStatement() error {
|
|
|
case p.check(lexer.TokenNewline):
|
|
|
p.advance() // Skip newlines
|
|
|
return nil
|
|
|
- case p.checkKeyword("title"):
|
|
|
- return p.parseTitle()
|
|
|
+ case p.checkKeyword("direction"):
|
|
|
+ return p.parseDirectionStatement()
|
|
|
case p.check(lexer.TokenID):
|
|
|
// Try to parse as entity or relationship
|
|
|
- return p.parseEntityOrRelation()
|
|
|
- case p.check(lexer.TokenString):
|
|
|
- // Entity name in quotes
|
|
|
- return p.parseEntityOrRelation()
|
|
|
+ return p.parseEntityOrRelationship()
|
|
|
default:
|
|
|
token := p.peek()
|
|
|
return p.error(fmt.Sprintf("unexpected token: %s", token.Value))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// parseTitle parses title statements
|
|
|
-func (p *ERParser) parseTitle() error {
|
|
|
- p.advance() // consume 'title'
|
|
|
+// parseEntityOrRelationship attempts to parse either an entity definition or a relationship
|
|
|
+func (p *ERParser) parseEntityOrRelationship() error {
|
|
|
+ entityName := p.advance().Value
|
|
|
|
|
|
- var titleParts []string
|
|
|
- for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
|
|
|
- titleParts = append(titleParts, p.advance().Value)
|
|
|
+ // Check if this is a relationship (has cardinality symbols)
|
|
|
+ if p.checkCardinality() {
|
|
|
+ return p.parseRelationship(entityName)
|
|
|
}
|
|
|
|
|
|
- if len(titleParts) > 0 {
|
|
|
- title := strings.TrimSpace(strings.Join(titleParts, " "))
|
|
|
- p.diagram.Title = &title
|
|
|
+ // Check if this is an entity with attributes (has {)
|
|
|
+ if p.check(lexer.TokenOpenBrace) {
|
|
|
+ return p.parseEntityWithAttributes(entityName)
|
|
|
}
|
|
|
|
|
|
+ // Simple entity without attributes
|
|
|
+ p.addEntity(entityName)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// parseEntityOrRelation parses either an entity definition or relationship
|
|
|
-func (p *ERParser) parseEntityOrRelation() error {
|
|
|
- // Get entity name
|
|
|
- entityName := p.advance().Value
|
|
|
+// parseRelationship parses a relationship between two entities
|
|
|
+func (p *ERParser) parseRelationship(fromEntity string) error {
|
|
|
+ // Parse relationship type (already tokenized as compound token)
|
|
|
+ relType, err := p.parseRelType()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
|
|
|
- // Remove quotes if present
|
|
|
- if strings.HasPrefix(entityName, "\"") && strings.HasSuffix(entityName, "\"") {
|
|
|
- entityName = entityName[1 : len(entityName)-1]
|
|
|
+ // Parse second entity
|
|
|
+ if !p.check(lexer.TokenID) {
|
|
|
+ return p.error("expected second entity name")
|
|
|
}
|
|
|
+ toEntity := p.advance().Value
|
|
|
|
|
|
- // Check for entity attributes block
|
|
|
- if p.check(lexer.TokenOpenBrace) {
|
|
|
- return p.parseEntityWithAttributes(entityName)
|
|
|
+ // Ensure both entities exist
|
|
|
+ p.addEntity(fromEntity)
|
|
|
+ p.addEntity(toEntity)
|
|
|
+
|
|
|
+ // Parse optional label
|
|
|
+ var label *string
|
|
|
+ if p.check(lexer.TokenColon) {
|
|
|
+ p.advance() // consume ':'
|
|
|
+ var labelParts []string
|
|
|
+ for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
|
|
|
+ labelParts = append(labelParts, p.advance().Value)
|
|
|
+ }
|
|
|
+ labelStr := strings.TrimSpace(strings.Join(labelParts, " "))
|
|
|
+ label = &labelStr
|
|
|
}
|
|
|
|
|
|
- // Check for relationship (cardinality indicators)
|
|
|
- if p.checkRelationship() {
|
|
|
- return p.parseRelationship(entityName)
|
|
|
+ // Create relationship
|
|
|
+ relation := &ast.ERRelation{
|
|
|
+ From: fromEntity,
|
|
|
+ To: toEntity,
|
|
|
+ Type: relType,
|
|
|
+ Label: label,
|
|
|
}
|
|
|
|
|
|
- // Just a standalone entity
|
|
|
- p.ensureEntity(entityName)
|
|
|
+ p.diagram.Relations = append(p.diagram.Relations, relation)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// parseEntityWithAttributes parses entity with attribute block
|
|
|
+// parseEntityWithAttributes parses an entity with attribute definitions
|
|
|
func (p *ERParser) parseEntityWithAttributes(entityName string) error {
|
|
|
- entity := p.ensureEntity(entityName)
|
|
|
-
|
|
|
p.advance() // consume '{'
|
|
|
|
|
|
- // Parse attributes until '}'
|
|
|
+ entity := p.addEntity(entityName)
|
|
|
+
|
|
|
+ // Parse attributes
|
|
|
for !p.check(lexer.TokenCloseBrace) && !p.isAtEnd() {
|
|
|
if p.check(lexer.TokenNewline) {
|
|
|
- p.advance() // Skip newlines
|
|
|
+ p.advance()
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- if err := p.parseAttribute(entity); err != nil {
|
|
|
+ attribute, err := p.parseAttribute()
|
|
|
+ if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
+ entity.Attributes = append(entity.Attributes, attribute)
|
|
|
}
|
|
|
|
|
|
if !p.check(lexer.TokenCloseBrace) {
|
|
|
- return p.error("expected '}'")
|
|
|
+ return p.error("expected '}' to close entity attributes")
|
|
|
}
|
|
|
p.advance() // consume '}'
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// parseAttribute parses entity attributes
|
|
|
-func (p *ERParser) parseAttribute(entity *ast.EREntity) error {
|
|
|
- // Parse: type name [key] ["comment"]
|
|
|
+// parseAttribute parses an attribute definition
|
|
|
+func (p *ERParser) parseAttribute() (*ast.ERAttribute, error) {
|
|
|
+ // Parse attribute type
|
|
|
if !p.check(lexer.TokenID) {
|
|
|
- return p.error("expected attribute type")
|
|
|
+ return nil, p.error("expected attribute type")
|
|
|
}
|
|
|
-
|
|
|
attrType := p.advance().Value
|
|
|
|
|
|
+ // Parse attribute name
|
|
|
if !p.check(lexer.TokenID) {
|
|
|
- return p.error("expected attribute name")
|
|
|
+ return nil, p.error("expected attribute name")
|
|
|
}
|
|
|
-
|
|
|
attrName := p.advance().Value
|
|
|
|
|
|
attribute := &ast.ERAttribute{
|
|
|
- Name: attrName,
|
|
|
Type: attrType,
|
|
|
+ Name: attrName,
|
|
|
}
|
|
|
|
|
|
- // Check for key constraint
|
|
|
- if p.check(lexer.TokenID) {
|
|
|
- keyWord := p.peek().Value
|
|
|
- if keyWord == "PK" || keyWord == "FK" || keyWord == "UK" {
|
|
|
- p.advance()
|
|
|
- switch keyWord {
|
|
|
- case "PK":
|
|
|
- key := ast.ERKeyPrimary
|
|
|
- attribute.Key = &key
|
|
|
- case "FK":
|
|
|
- key := ast.ERKeyForeign
|
|
|
- attribute.Key = &key
|
|
|
- case "UK":
|
|
|
- key := ast.ERKeyUnique
|
|
|
- attribute.Key = &key
|
|
|
- }
|
|
|
- }
|
|
|
+ // Parse optional key (PK, FK, UK)
|
|
|
+ if p.check(lexer.TokenID) && p.isKeyWord() {
|
|
|
+ keyStr := p.advance().Value
|
|
|
+ key := ast.ERKeyType(keyStr)
|
|
|
+ attribute.Key = &key
|
|
|
}
|
|
|
|
|
|
- // Check for comment
|
|
|
+ // Parse optional comment (quoted string)
|
|
|
if p.check(lexer.TokenString) {
|
|
|
comment := p.advance().Value
|
|
|
// Remove quotes
|
|
|
@@ -205,156 +212,173 @@ func (p *ERParser) parseAttribute(entity *ast.EREntity) error {
|
|
|
attribute.Comment = &comment
|
|
|
}
|
|
|
|
|
|
- entity.Attributes = append(entity.Attributes, attribute)
|
|
|
- return nil
|
|
|
+ return attribute, nil
|
|
|
}
|
|
|
|
|
|
-// parseRelationship parses entity relationships
|
|
|
-func (p *ERParser) parseRelationship(fromEntity string) error {
|
|
|
- // Ensure from entity exists
|
|
|
- p.ensureEntity(fromEntity)
|
|
|
-
|
|
|
- // Parse relationship specification
|
|
|
- relType, err := p.parseRelationshipSpec()
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
-
|
|
|
- // Parse to entity
|
|
|
- if !p.check(lexer.TokenID) && !p.check(lexer.TokenString) {
|
|
|
- return p.error("expected target entity")
|
|
|
- }
|
|
|
-
|
|
|
- toEntity := p.advance().Value
|
|
|
- // Remove quotes if present
|
|
|
- if strings.HasPrefix(toEntity, "\"") && strings.HasSuffix(toEntity, "\"") {
|
|
|
- toEntity = toEntity[1 : len(toEntity)-1]
|
|
|
- }
|
|
|
-
|
|
|
- // Ensure to entity exists
|
|
|
- p.ensureEntity(toEntity)
|
|
|
-
|
|
|
- // Parse role/label
|
|
|
- var label *string
|
|
|
- if p.check(lexer.TokenColon) {
|
|
|
- p.advance() // consume ':'
|
|
|
- var labelParts []string
|
|
|
- for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
|
|
|
- labelParts = append(labelParts, p.advance().Value)
|
|
|
- }
|
|
|
- if len(labelParts) > 0 {
|
|
|
- labelText := strings.TrimSpace(strings.Join(labelParts, " "))
|
|
|
- // Remove quotes if present
|
|
|
- if strings.HasPrefix(labelText, "\"") && strings.HasSuffix(labelText, "\"") {
|
|
|
- labelText = labelText[1 : len(labelText)-1]
|
|
|
- }
|
|
|
- label = &labelText
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- relation := &ast.ERRelation{
|
|
|
- From: fromEntity,
|
|
|
- To: toEntity,
|
|
|
- Type: relType,
|
|
|
- Label: label,
|
|
|
+// parseRelType parses relationship type symbols
|
|
|
+func (p *ERParser) parseRelType() (ast.ERRelationType, error) {
|
|
|
+ if p.isAtEnd() {
|
|
|
+ return "", p.error("expected relationship type")
|
|
|
}
|
|
|
|
|
|
- p.diagram.Relations = append(p.diagram.Relations, relation)
|
|
|
- return nil
|
|
|
-}
|
|
|
+ token := p.peek()
|
|
|
|
|
|
-// parseRelationshipSpec parses relationship specification
|
|
|
-func (p *ERParser) parseRelationshipSpec() (ast.ERRelationType, error) {
|
|
|
- // Check for ER relationship tokens first
|
|
|
- if p.check(lexer.TokenEROneToMany) {
|
|
|
+ // Check for compound ER relationship tokens first
|
|
|
+ switch token.Type {
|
|
|
+ case lexer.TokenEROneToMany:
|
|
|
p.advance()
|
|
|
return ast.ERRelationOneToMany, nil
|
|
|
- }
|
|
|
- if p.check(lexer.TokenERManyToOne) {
|
|
|
+ case lexer.TokenEROneToManyAlt:
|
|
|
+ p.advance()
|
|
|
+ return ast.ERRelationOneToManyAlt, nil
|
|
|
+ case lexer.TokenERManyToOne:
|
|
|
p.advance()
|
|
|
return ast.ERRelationManyToOne, nil
|
|
|
- }
|
|
|
- if p.check(lexer.TokenEROneToOne) {
|
|
|
+ case lexer.TokenEROneToOne:
|
|
|
p.advance()
|
|
|
return ast.ERRelationOneToOne, nil
|
|
|
- }
|
|
|
- if p.check(lexer.TokenERManyToMany) {
|
|
|
+ case lexer.TokenERManyToMany:
|
|
|
p.advance()
|
|
|
return ast.ERRelationManyToMany, nil
|
|
|
- }
|
|
|
- if p.check(lexer.TokenERZeroToOne) {
|
|
|
+ case lexer.TokenERManyToManyAlt:
|
|
|
+ p.advance()
|
|
|
+ return ast.ERRelationManyToManyAlt, nil
|
|
|
+ case lexer.TokenERZeroToOne:
|
|
|
p.advance()
|
|
|
return ast.ERRelationZeroToOne, nil
|
|
|
}
|
|
|
|
|
|
- // Fallback: build the relationship string by consuming tokens until we find the target entity
|
|
|
- var relationParts []string
|
|
|
-
|
|
|
- // Consume tokens until we find the next entity (ID or String)
|
|
|
- for !p.isAtEnd() && !p.check(lexer.TokenID) && !p.check(lexer.TokenString) {
|
|
|
- token := p.advance()
|
|
|
- relationParts = append(relationParts, token.Value)
|
|
|
- }
|
|
|
-
|
|
|
- if len(relationParts) == 0 {
|
|
|
- return ast.ERRelationOneToMany, fmt.Errorf("no relationship operator found")
|
|
|
+ // Fall back to individual token parsing for patterns not covered by compound tokens
|
|
|
+ // Look ahead to match relationship patterns
|
|
|
+ if p.matchString("||--||") {
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ return ast.ERRelationOneToOne, nil
|
|
|
}
|
|
|
-
|
|
|
- // Join the parts to form the complete relationship operator
|
|
|
- relationOp := strings.Join(relationParts, "")
|
|
|
-
|
|
|
- // Map common ER relationship patterns
|
|
|
- switch relationOp {
|
|
|
- case "||--o{":
|
|
|
+ if p.matchString("||--o{") {
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '{'
|
|
|
return ast.ERRelationOneToMany, nil
|
|
|
- case "}o--||":
|
|
|
+ }
|
|
|
+ if p.matchString("}o--||") {
|
|
|
+ p.advance() // consume '}'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
return ast.ERRelationManyToOne, nil
|
|
|
- case "||--||":
|
|
|
- return ast.ERRelationOneToOne, nil
|
|
|
- case "}o--o{":
|
|
|
+ }
|
|
|
+ if p.matchString("}o--o{") {
|
|
|
+ p.advance() // consume '}'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '{'
|
|
|
return ast.ERRelationManyToMany, nil
|
|
|
- case "||--o|":
|
|
|
+ }
|
|
|
+ if p.matchString("||--o|") {
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume '-'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '|'
|
|
|
return ast.ERRelationZeroToOne, nil
|
|
|
- default:
|
|
|
- // Default to one-to-many for unrecognized patterns
|
|
|
- return ast.ERRelationOneToMany, nil
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-// checkRelationship checks if current position looks like a relationship
|
|
|
-func (p *ERParser) checkRelationship() bool {
|
|
|
- // Check for ER relationship tokens
|
|
|
- if p.check(lexer.TokenEROneToMany) || p.check(lexer.TokenERManyToOne) ||
|
|
|
- p.check(lexer.TokenEROneToOne) || p.check(lexer.TokenERManyToMany) ||
|
|
|
- p.check(lexer.TokenERZeroToOne) {
|
|
|
- return true
|
|
|
+ if p.matchString("}o..o{") {
|
|
|
+ p.advance() // consume '}'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '.'
|
|
|
+ p.advance() // consume '.'
|
|
|
+ p.advance() // consume 'o'
|
|
|
+ p.advance() // consume '{'
|
|
|
+ return ast.ERRelationManyToMany, nil
|
|
|
+ }
|
|
|
+ if p.matchString("||..||") {
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '.'
|
|
|
+ p.advance() // consume '.'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ p.advance() // consume '|'
|
|
|
+ return ast.ERRelationOneToOne, nil
|
|
|
}
|
|
|
|
|
|
- // Check for old-style relationship patterns
|
|
|
- token := p.peek()
|
|
|
- return strings.Contains(token.Value, "||") ||
|
|
|
- strings.Contains(token.Value, "}") ||
|
|
|
- strings.Contains(token.Value, "o") ||
|
|
|
- strings.Contains(token.Value, "--")
|
|
|
+ return "", p.error("unrecognized relationship pattern")
|
|
|
}
|
|
|
|
|
|
-// ensureEntity ensures an entity exists, creating it if needed
|
|
|
-func (p *ERParser) ensureEntity(name string) *ast.EREntity {
|
|
|
- if entity, exists := p.diagram.Entities[name]; exists {
|
|
|
+// Helper methods
|
|
|
+func (p *ERParser) addEntity(name string) *ast.EREntity {
|
|
|
+ if entity, exists := p.entityMap[name]; exists {
|
|
|
return entity
|
|
|
}
|
|
|
|
|
|
entity := &ast.EREntity{
|
|
|
- ID: name,
|
|
|
+ ID: fmt.Sprintf("entity-%s-%d", name, len(p.diagram.Entities)),
|
|
|
Name: name,
|
|
|
Attributes: make([]*ast.ERAttribute, 0),
|
|
|
- CssClasses: make([]string, 0),
|
|
|
+ CssClasses: []string{"default"},
|
|
|
}
|
|
|
- p.diagram.Entities[name] = entity
|
|
|
+
|
|
|
+ p.entityMap[name] = entity
|
|
|
+ p.diagram.Entities = append(p.diagram.Entities, entity)
|
|
|
return entity
|
|
|
}
|
|
|
|
|
|
-// Helper methods
|
|
|
+func (p *ERParser) checkCardinality() bool {
|
|
|
+ if p.isAtEnd() {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ token := p.peek()
|
|
|
+
|
|
|
+ // Check for compound ER relationship tokens
|
|
|
+ switch token.Type {
|
|
|
+ case lexer.TokenEROneToMany, lexer.TokenEROneToManyAlt, lexer.TokenERManyToOne,
|
|
|
+ lexer.TokenEROneToOne, lexer.TokenERManyToMany, lexer.TokenERManyToManyAlt, lexer.TokenERZeroToOne:
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fall back to string matching for patterns not covered by compound tokens
|
|
|
+ return p.matchString("||--||") || p.matchString("||--o{") || p.matchString("}o--||") ||
|
|
|
+ p.matchString("}o--o{") || p.matchString("||--o|") || p.matchString("}o..o{") ||
|
|
|
+ p.matchString("||..||")
|
|
|
+}
|
|
|
+
|
|
|
+func (p *ERParser) isKeyWord() bool {
|
|
|
+ if p.isAtEnd() {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ token := p.peek()
|
|
|
+ return token.Type == lexer.TokenID && (token.Value == "PK" || token.Value == "FK" || token.Value == "UK")
|
|
|
+}
|
|
|
+
|
|
|
+func (p *ERParser) matchString(s string) bool {
|
|
|
+ if p.current+len(s)-1 >= len(p.tokens) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ var actual strings.Builder
|
|
|
+ for i := 0; i < len(s); i++ {
|
|
|
+ if p.current+i >= len(p.tokens) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ actual.WriteString(p.tokens[p.current+i].Value)
|
|
|
+ }
|
|
|
+
|
|
|
+ return actual.String() == s
|
|
|
+}
|
|
|
+
|
|
|
func (p *ERParser) check(tokenType lexer.TokenType) bool {
|
|
|
if p.isAtEnd() {
|
|
|
return false
|
|
|
@@ -367,7 +391,7 @@ func (p *ERParser) checkKeyword(keyword string) bool {
|
|
|
return false
|
|
|
}
|
|
|
token := p.peek()
|
|
|
- return token.Type == lexer.TokenID && strings.ToLower(token.Value) == strings.ToLower(keyword)
|
|
|
+ return token.Type == lexer.TokenID && strings.EqualFold(token.Value, keyword)
|
|
|
}
|
|
|
|
|
|
func (p *ERParser) advance() lexer.Token {
|
|
|
@@ -395,6 +419,20 @@ func (p *ERParser) previous() lexer.Token {
|
|
|
return p.tokens[p.current-1]
|
|
|
}
|
|
|
|
|
|
+func (p *ERParser) parseDirectionStatement() error {
|
|
|
+ p.advance() // consume 'direction'
|
|
|
+
|
|
|
+ if !p.check(lexer.TokenID) {
|
|
|
+ return p.error("expected direction (TB, BT, RL, LR)")
|
|
|
+ }
|
|
|
+
|
|
|
+ // For now, we'll just consume the direction token
|
|
|
+ // The existing ERDiagram struct doesn't have a Direction field
|
|
|
+ p.advance()
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
func (p *ERParser) error(message string) error {
|
|
|
token := p.peek()
|
|
|
return fmt.Errorf("parse error at line %d, column %d: %s (got %s)",
|