// SPDX-License-Identifier: AGPL-3.0-only package compiler import ( "bytes" "fmt" "go/ast" "go/parser" "go/printer" "go/scanner" "go/token" "path/filepath" "sort" "strconv" "strings" "unicode/utf8" ) const headerOpen = " t.size { offset = t.size } lineIndex := sort.Search(len(t.lineStarts), func(i int) bool { return t.lineStarts[i] > offset }) - 1 if lineIndex < 0 { lineIndex = 0 } return sourcePosition{ Offset: offset, Line: lineIndex + 1, Column: offset - t.lineStarts[lineIndex] + 1, } } func parseSource(path string, source []byte) (*sourceFile, []Diagnostic) { table := newPositionTable(source) var diagnostics []Diagnostic if !utf8.Valid(source) { diagnostics = append(diagnostics, diagnostic(path, table.at(0), "HIM1001", "source is not valid UTF-8")) return nil, diagnostics } if offset := bytes.IndexByte(source, 0); offset >= 0 { diagnostics = append(diagnostics, diagnostic(path, table.at(offset), "HIM1002", "NUL bytes are not permitted in .sando sources")) return nil, diagnostics } headerStart := 0 if bytes.HasPrefix(source, []byte{0xef, 0xbb, 0xbf}) { headerStart = 3 } for headerStart < len(source) && isSpace(source[headerStart]) { headerStart++ } if !bytes.HasPrefix(source[headerStart:], []byte(headerOpen)) { diagnostics = append(diagnostics, diagnostic(path, table.at(headerStart), "HIM1101", "file must begin (after optional UTF-8 BOM and whitespace) with a = len(source) || !isSpace(source[afterMarker]) { diagnostics = append(diagnostics, diagnostic(path, table.at(afterMarker), "HIM1105", "whitespace is required between 2 && !isSpace(directiveBody[2])) { diagnostics = append(diagnostics, diagnostic(path, table.at(directiveStart), "HIM1103", "unsupported or missing header target; v1 requires begins the template body")) } } if hasErrors(diagnostics) { return nil, diagnostics } formattedType, formatErr := formatNode(fset, function.Type) if formatErr != nil { diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1120", "could not format component parameters: "+formatErr.Error())) return nil, diagnostics } typeParams, params, splitErr := splitFormattedFuncType(formattedType) if splitErr != nil { diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1121", "could not recover formatted component signature: "+splitErr.Error())) return nil, diagnostics } sort.SliceStable(imports, func(i, j int) bool { if imports[i].Path != imports[j].Path { return imports[i].Path < imports[j].Path } return imports[i].Alias < imports[j].Alias }) for index := 1; index < len(imports); index++ { if imports[index-1].Path == imports[index].Path { diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset), "HIM1122", fmt.Sprintf("import path %q is declared more than once", imports[index].Path))) } } if hasErrors(diagnostics) { return nil, diagnostics } return &parsedHeader{ Package: parsed.Name.Name, Name: function.Name.Name, TypeParams: typeParams, Params: params, Imports: imports, AST: parsed, }, diagnostics } func insertSyntheticFunctionBody(declarations []byte) ([]byte, int) { trimmed := bytes.TrimRight(declarations, " \t\r\n") fileSet := token.NewFileSet() file := fileSet.AddFile("header.go", -1, len(trimmed)) var lexical scanner.Scanner lexical.Init(file, trimmed, nil, scanner.ScanComments) seenFunction := false squareDepth := 0 curlyDepth := 0 parameterDepth := 0 parameterListStarted := false insertion := -1 for { position, tok, _ := lexical.Scan() if tok == token.EOF { break } if !seenFunction { if tok == token.FUNC { seenFunction = true } continue } if parameterListStarted { switch tok { case token.LPAREN: parameterDepth++ case token.RPAREN: parameterDepth-- if parameterDepth == 0 { insertion = fileSet.Position(position).Offset + 1 } } if insertion >= 0 { break } continue } switch tok { case token.LBRACK: squareDepth++ case token.RBRACK: if squareDepth > 0 { squareDepth-- } case token.LBRACE: curlyDepth++ case token.RBRACE: if curlyDepth > 0 { curlyDepth-- } case token.LPAREN: if squareDepth == 0 && curlyDepth == 0 { parameterListStarted = true parameterDepth = 1 } } } if insertion < 0 || insertion > len(trimmed) { insertion = len(trimmed) } parseInput := make([]byte, 0, len(trimmed)+3) parseInput = append(parseInput, trimmed[:insertion]...) parseInput = append(parseInput, ' ', '{', '}') parseInput = append(parseInput, trimmed[insertion:]...) return parseInput, insertion + 1 } func splitFormattedFuncType(formatted string) (typeParams, params string, err error) { remainder := strings.TrimSpace(strings.TrimPrefix(formatted, "func")) bracketDepth := 0 quote := byte(0) escaped := false for index := 0; index < len(remainder); index++ { b := remainder[index] if quote != 0 { if quote != '`' && escaped { escaped = false continue } if quote != '`' && b == '\\' { escaped = true continue } if b == quote { quote = 0 } continue } if b == '"' || b == '\'' || b == '`' { quote = b continue } switch b { case '[': bracketDepth++ case ']': if bracketDepth > 0 { bracketDepth-- } case '(': if bracketDepth == 0 { return strings.TrimSpace(remainder[:index]), strings.TrimSpace(remainder[index:]), nil } } } return "", "", fmt.Errorf("formatted function type has no parameter list") } func formatNode(fset *token.FileSet, node any) (string, error) { var output bytes.Buffer configuration := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} if err := configuration.Fprint(&output, fset, node); err != nil { return "", err } return output.String(), nil } func parserDiagnostics(path string, err error, sourceOffset int, table positionTable, code, prefix string) []Diagnostic { var diagnostics []Diagnostic if list, ok := err.(scanner.ErrorList); ok { for _, parseError := range list { offset := sourceOffset + parseError.Pos.Offset diagnostics = append(diagnostics, diagnostic(path, table.at(offset), code, prefix+": "+parseError.Msg)) } return diagnostics } return []Diagnostic{diagnostic(path, table.at(sourceOffset), code, prefix+": "+err.Error())} } func tokenizeTemplate(file *sourceFile, template []byte, sourceOffset int, table positionTable) []Diagnostic { var diagnostics []Diagnostic cursor := 0 for cursor < len(template) { openRelative := bytes.Index(template[cursor:], []byte(" cursor { file.Nodes = append(file.Nodes, rendererNode{Kind: nodeText, Text: string(template[cursor:open]), Pos: table.at(sourceOffset + cursor)}) } close := -1 if open+2 < len(template) && template[open+2] == '#' { if closeRelative := bytes.Index(template[open+2:], []byte("?>")); closeRelative >= 0 { close = open + 2 + closeRelative } } else { close = findGoDelimiter(template, open+2) } if close < 0 { diagnostics = append(diagnostics, diagnostic(file.Path, table.at(sourceOffset+open), "HIM1201", "unterminated template tag")) break } kind := nodeStatement contentStart := open + 2 if contentStart < close { switch template[contentStart] { case '=': kind = nodeExpression contentStart++ case '~': kind = nodeComponent contentStart++ case '#': kind = nodeComment contentStart++ } } content := template[contentStart:close] trimmed := bytes.TrimSpace(content) trimLeading := len(content) - len(bytes.TrimLeft(content, " \t\r\n")) position := table.at(sourceOffset + contentStart + trimLeading) if bytes.HasPrefix(bytes.TrimSpace(template[open+2:close]), []byte("sando")) { diagnostics = append(diagnostics, diagnostic(file.Path, table.at(sourceOffset+open), "HIM1202", " outside Go strings, rune literals, raw // strings, and comments. Statement tags may be structurally incomplete across // template regions, so requiring each region to parse independently would // reject ordinary `if { ?>...': return index case b == '"': state = lexicalString escaped = false case b == '\'': state = lexicalRune escaped = false case b == '`': state = lexicalRawString case b == '/' && next == '/': state = lexicalLineComment index++ case b == '/' && next == '*': state = lexicalBlockComment index++ } case lexicalString, lexicalRune: if escaped { escaped = false continue } if b == '\\' { escaped = true continue } if (state == lexicalString && b == '"') || (state == lexicalRune && b == '\'') { state = lexicalNormal } case lexicalRawString: if b == '`' { state = lexicalNormal } case lexicalLineComment: if b == '\n' { state = lexicalNormal } case lexicalBlockComment: if b == '*' && next == '/' { state = lexicalNormal index++ } } } return -1 } func expressionDiagnostics(path string, err error, fallback sourcePosition, table positionTable, sourceOffset int) []Diagnostic { if list, ok := err.(scanner.ErrorList); ok { diagnostics := make([]Diagnostic, 0, len(list)) for _, parseError := range list { diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+parseError.Pos.Offset), "HIM1210", "invalid Go expression: "+parseError.Msg)) } return diagnostics } return []Diagnostic{diagnostic(path, fallback, "HIM1210", "invalid Go expression: "+strings.TrimSpace(err.Error()))} }