Preserve Gotlin semantics across packages

This commit is contained in:
pavel 2026-08-27 22:18:45 +02:00
commit acb42a5702
10 changed files with 525 additions and 34 deletions

View file

@ -326,27 +326,43 @@ func (s *server) buildDocumentState(uri, text string) documentState {
return state
}
state.program = program
programs, symbols := s.packageContext(uri, program.PackagePath, text)
_, semanticDiagnostics := lang.AnalyzeWithContext(program, programs)
programs, symbols, metadata := s.packageContext(uri, program.PackagePath, text)
_, semanticDiagnostics := lang.AnalyzeWithContextAndMetadata(program, programs, metadata)
state.symbols = indexSymbols(text, program)
state.packageSymbols = symbols
state.diagnostics = diagnosticsFromSemantic(text, semanticDiagnostics)
return state
}
func (s *server) packageContext(currentURI, packagePath, currentText string) ([]*lang.Program, []symbol) {
func (s *server) packageContext(currentURI, packagePath, currentText string) ([]*lang.Program, []symbol, []*lang.PackageMetadata) {
path, ok := filePathFromURI(currentURI)
if !ok {
return nil, nil
return nil, nil, nil
}
root := nearestWorkspaceRoot(filepath.Dir(path))
if root == "" {
root = nearestModuleRoot(filepath.Dir(path))
}
root := nearestModuleRoot(filepath.Dir(path))
if root == "" {
root = filepath.Dir(path)
}
var programs []*lang.Program
var symbols []symbol
var metadata []*lang.PackageMetadata
_ = filepath.WalkDir(root, func(candidate string, entry os.DirEntry, err error) error {
if err != nil || entry.IsDir() || filepath.Ext(candidate) != ".gt" {
if err != nil || entry.IsDir() {
return nil
}
if strings.HasSuffix(candidate, ".gti.json") {
if body, readErr := os.ReadFile(candidate); readErr == nil {
var item lang.PackageMetadata
if json.Unmarshal(body, &item) == nil && item.Version == lang.PackageMetadataVersion {
metadata = append(metadata, &item)
}
}
return nil
}
if filepath.Ext(candidate) != ".gt" {
return nil
}
candidateURI := fileURI(candidate)
@ -374,7 +390,20 @@ func (s *server) packageContext(currentURI, packagePath, currentText string) ([]
}
return nil
})
return programs, symbols
return programs, symbols, metadata
}
func nearestWorkspaceRoot(directory string) string {
for {
if _, err := os.Stat(filepath.Join(directory, "go.work")); err == nil {
return directory
}
parent := filepath.Dir(directory)
if parent == directory {
return ""
}
directory = parent
}
}
func nearestModuleRoot(directory string) string {

View file

@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
@ -32,6 +33,10 @@ func runBuild(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError)
srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable")
outPath := fs.String("o", "", "output file path")
metadataOutput := fs.String("metadata-output", "", "write Gotlin package metadata")
metadataPackage := fs.String("metadata-package", "", "import path recorded in package metadata")
var metadataInputs stringListFlag
fs.Var(&metadataInputs, "metadata", "Gotlin package metadata dependency (repeatable)")
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] <input.gt>")
}
@ -48,7 +53,7 @@ func runBuild(args []string) {
}
inputPath := fs.Arg(0)
goSrc := compileFiles(fs.Args(), !*srcOnly)
goSrc := compileFilesWithMetadata(fs.Args(), !*srcOnly, metadataInputs, *metadataOutput, *metadataPackage)
if *srcOnly {
if *outPath == "" {
@ -117,6 +122,10 @@ func compileFile(inputPath string, forceMain bool) string {
}
func compileFiles(inputPaths []string, forceMain bool) string {
return compileFilesWithMetadata(inputPaths, forceMain, nil, "", "")
}
func compileFilesWithMetadata(inputPaths []string, forceMain bool, metadataPaths []string, metadataOutput, metadataPackage string) string {
program := &lang.Program{}
var firstSource string
for _, inputPath := range inputPaths {
@ -146,12 +155,27 @@ func compileFiles(inputPaths []string, forceMain bool) string {
firstSource = string(src)
}
}
metadata := loadMetadata(metadataPaths)
if metadataOutput != "" {
packageMetadata, err := lang.BuildPackageMetadata(program, metadataPackage)
if err != nil {
fail(err)
}
encoded, err := json.MarshalIndent(packageMetadata, "", " ")
if err != nil {
fail(err)
}
encoded = append(encoded, '\n')
if err := os.WriteFile(metadataOutput, encoded, 0o644); err != nil {
fail(err)
}
}
var goSrc []byte
var err error
if forceMain {
goSrc, err = lang.GenerateGoMain(program)
goSrc, err = lang.GenerateGoMainWithMetadata(program, metadata)
} else {
goSrc, err = lang.GenerateGo(program)
goSrc, err = lang.GenerateGoWithMetadata(program, metadata)
}
if err != nil {
fail(err)
@ -162,6 +186,30 @@ func compileFiles(inputPaths []string, forceMain bool) string {
return string(goSrc)
}
type stringListFlag []string
func (values *stringListFlag) String() string { return strings.Join(*values, ",") }
func (values *stringListFlag) Set(value string) error { *values = append(*values, value); return nil }
func loadMetadata(paths []string) []*lang.PackageMetadata {
metadata := make([]*lang.PackageMetadata, 0, len(paths))
for _, path := range paths {
body, err := os.ReadFile(path)
if err != nil {
fail(err)
}
var item lang.PackageMetadata
if err := json.Unmarshal(body, &item); err != nil {
fail(fmt.Errorf("load metadata %s: %w", path, err))
}
if item.Version != lang.PackageMetadataVersion {
fail(fmt.Errorf("metadata %s uses unsupported version %d", path, item.Version))
}
metadata = append(metadata, &item)
}
return metadata
}
func buildExecutable(goSrc string, outputPath string) error {
tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go")
if err != nil {
@ -202,7 +250,7 @@ func normalizeBuildArgs(args []string) ([]string, error) {
switch {
case arg == "-src":
flags = append(flags, arg)
case arg == "-o":
case arg == "-o" || arg == "-metadata" || arg == "-metadata-output" || arg == "-metadata-package":
if i+1 >= len(args) {
return nil, fmt.Errorf("missing value for -o")
}