146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
package syntax
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
shared "plandex-shared"
|
|
|
|
tree_sitter "github.com/smacker/go-tree-sitter"
|
|
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
const parserTimeout = 500 * time.Millisecond
|
|
|
|
type ValidationRes = struct {
|
|
Lang shared.Language
|
|
Parser *tree_sitter.Parser
|
|
TimedOut bool
|
|
Valid bool
|
|
Errors []string
|
|
}
|
|
|
|
func ValidateFile(ctx context.Context, path string, file string) (*ValidationRes, error) {
|
|
parser, lang, fallbackParser, fallbackLang := GetParserForPath(path)
|
|
|
|
if parser == nil {
|
|
return &ValidationRes{Lang: lang, Parser: nil}, nil
|
|
}
|
|
|
|
return ValidateWithParsers(ctx, lang, parser, fallbackLang, fallbackParser, file)
|
|
}
|
|
|
|
func ValidateWithParsers(ctx context.Context, lang shared.Language, parser *tree_sitter.Parser, fallbackLang shared.Language, fallbackParser *tree_sitter.Parser, file string) (*ValidationRes, error) {
|
|
if file == "" {
|
|
return &ValidationRes{Lang: lang, Parser: parser, Valid: true}, nil
|
|
}
|
|
|
|
// Set a timeout duration for the parsing operations
|
|
ctx, cancel := context.WithTimeout(ctx, parserTimeout)
|
|
defer cancel()
|
|
|
|
// Parse the content
|
|
tree, err := parser.ParseCtx(ctx, nil, []byte(file))
|
|
|
|
if err != nil || tree == nil {
|
|
if err != nil && err.Error() == "operation limit was hit" {
|
|
return &ValidationRes{Lang: lang, Parser: parser, TimedOut: true}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to parse the content: %v", err)
|
|
}
|
|
defer tree.Close()
|
|
|
|
// Get the root node of the syntax tree and check for errors
|
|
root := tree.RootNode()
|
|
|
|
if root.HasError() {
|
|
if fallbackParser != nil {
|
|
fallbackTree, err := fallbackParser.ParseCtx(ctx, nil, []byte(file))
|
|
if err != nil || fallbackTree == nil {
|
|
|
|
if err != nil && strings.Contains(err.Error(), "timeout") {
|
|
return &ValidationRes{Lang: lang, Parser: parser, TimedOut: true}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to parse the content with fallback parser: %v", err)
|
|
}
|
|
defer fallbackTree.Close()
|
|
|
|
root = fallbackTree.RootNode()
|
|
|
|
if !root.HasError() {
|
|
return &ValidationRes{Lang: fallbackLang, Parser: fallbackParser, Valid: true}, nil
|
|
}
|
|
}
|
|
|
|
errorMarkers := insertErrorMarkers(file, root)
|
|
|
|
return &ValidationRes{
|
|
Lang: lang,
|
|
Parser: parser,
|
|
Valid: false,
|
|
Errors: errorMarkers,
|
|
}, nil
|
|
|
|
}
|
|
|
|
return &ValidationRes{Lang: lang, Parser: parser, Valid: true}, nil
|
|
}
|
|
|
|
func insertErrorMarkers(source string, node *tree_sitter.Node) []string {
|
|
if source == "" {
|
|
return []string{}
|
|
}
|
|
|
|
var markers []string
|
|
var uniqueMarkers = map[string]bool{}
|
|
|
|
// Function to calculate line numbers
|
|
calculateLineNumber := func(position int) int {
|
|
return strings.Count(source[:position], "\n") + 1
|
|
}
|
|
|
|
hasChildError := func(n *tree_sitter.Node) bool {
|
|
for i := 0; i < int(n.ChildCount()); i++ {
|
|
if n.Child(i).HasError() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
visitNodes(node, func(n *tree_sitter.Node) {
|
|
if n.HasError() && !hasChildError(n) {
|
|
startPosition := int(n.StartByte())
|
|
endPosition := int(n.EndByte())
|
|
startLineNumber := calculateLineNumber(startPosition)
|
|
endLineNumber := calculateLineNumber(endPosition)
|
|
|
|
if startLineNumber == endLineNumber {
|
|
uniqueMarkers[fmt.Sprintf("Invalid syntax on line %d", startLineNumber)] = true
|
|
} else {
|
|
uniqueMarkers[fmt.Sprintf("Invalid syntax on lines %d to %d", startLineNumber, endLineNumber)] = true
|
|
}
|
|
|
|
}
|
|
})
|
|
|
|
for marker := range uniqueMarkers {
|
|
markers = append(markers, marker)
|
|
}
|
|
|
|
return markers
|
|
}
|
|
|
|
// visitNodes recursively visits nodes in the syntax tree
|
|
func visitNodes(n *tree_sitter.Node, f func(node *tree_sitter.Node)) {
|
|
f(n)
|
|
for i := 0; i < int(n.ChildCount()); i++ {
|
|
child := n.Child(i)
|
|
visitNodes(child, f)
|
|
}
|
|
}
|