Skip to content

Commit

Permalink
feat(tools/spxls): implement spx resource reference check
Browse files Browse the repository at this point in the history
Updates goplus#1059

Signed-off-by: Aofei Sheng <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Nov 27, 2024
1 parent 484dd28 commit 3a53e0c
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 7 deletions.
229 changes: 222 additions & 7 deletions tools/spxls/internal/server/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"errors"
"fmt"
"go/types"
"io/fs"
"path/filepath"
"strings"

"github.com/goplus/builder/tools/spxls/internal"
"github.com/goplus/builder/tools/spxls/internal/vfs"
Expand Down Expand Up @@ -66,11 +69,11 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
fset := goptoken.NewFileSet()
gpfs := vfs.NewGopParserFS(s.workspaceRootFS)
mainPkgFiles := make(map[string]*gopast.File)
for _, file := range spxFiles {
documentURI := s.toDocumentURI(file)
for _, spxFile := range spxFiles {
documentURI := s.toDocumentURI(spxFile)
diags[documentURI] = nil

f, err := gopparser.ParseFSEntry(fset, gpfs, file, nil, gopparser.Config{
f, err := gopparser.ParseFSEntry(fset, gpfs, spxFile, nil, gopparser.Config{
Mode: gopparser.AllErrors | gopparser.ParseComments,
})
if err != nil {
Expand All @@ -79,6 +82,7 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
if errors.As(err, &parseErr) {
for _, e := range parseErr {
diags[documentURI] = append(diags[documentURI], Diagnostic{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(e.Pos),
End: FromGopTokenPosition(e.Pos),
Expand All @@ -94,6 +98,7 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
if errors.As(err, &codeErr) {
position := codeErr.Fset.Position(codeErr.Pos)
diags[documentURI] = append(diags[documentURI], Diagnostic{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(position),
End: FromGopTokenPosition(position),
Expand All @@ -115,7 +120,7 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
continue
}
if f.Name.Name == "main" {
mainPkgFiles[file] = f
mainPkgFiles[spxFile] = f
}
}
if len(mainPkgFiles) == 0 {
Expand All @@ -142,12 +147,12 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
position := typeErr.Fset.Position(typeErr.Pos)
documentURI := s.toDocumentURI(position.Filename)
diags[documentURI] = append(diags[documentURI], Diagnostic{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(position),
End: FromGopTokenPosition(position),
},
Message: typeErr.Msg,
Severity: SeverityError,
Message: typeErr.Msg,
})
}
},
Expand All @@ -164,7 +169,217 @@ func (s *Server) diagnose() (map[DocumentURI][]Diagnostic, error) {
// Errors should be handled by the type checker.
}

// TODO: spx resource reference check.
for spxFile, gopastFile := range mainPkgFiles {
documentURI := s.toDocumentURI(spxFile)
gopast.Inspect(gopastFile, func(node gopast.Node) bool {
callExpr, ok := node.(*gopast.CallExpr)
if !ok {
return true
}

var fName string
switch fun := callExpr.Fun.(type) {
case *gopast.Ident:
fName = fun.Name
case *gopast.SelectorExpr:
fName = fun.Sel.Name
default:
return true
}

switch fName {
case "play":
subDiags := s.validateSpxGamePlayCall(typeInfo, callExpr, fset)
if subDiags != nil {
diags[documentURI] = append(diags[documentURI], subDiags...)
}
case "animate":
subDiags := s.validateSpxSpriteAnimateCall(typeInfo, callExpr, fset)
if subDiags != nil {
diags[documentURI] = append(diags[documentURI], subDiags...)
}
}
return true
})
}
return diags, nil
}

// validateSpxGamePlayCall validates a spx.Game.play call.
//
// See https://pkg.go.dev/github.com/goplus/spx#Game.Play__0
func (s *Server) validateSpxGamePlayCall(typeInfo *goptypesutil.Info, callExpr *gopast.CallExpr, fset *goptoken.FileSet) []Diagnostic {
tv, ok := typeInfo.Types[callExpr.Fun]
if !ok {
return nil
}
sig, ok := tv.Type.(*types.Signature)
if !ok {
return nil
}
recv := sig.Recv()
if recv == nil {
return nil
}
recvType := recv.Type()
if ptr, ok := recvType.(*types.Pointer); ok {
recvType = ptr.Elem()
}
if recvType.String() != "github.com/goplus/spx.Game" {
return nil
}

if len(callExpr.Args) == 0 {
return nil
}
argTV, ok := typeInfo.Types[callExpr.Args[0]]
if !ok {
return nil
}
if !types.Identical(argTV.Type, types.Typ[types.String]) {
return nil
}

var soundName string
switch arg := callExpr.Args[0].(type) {
case *gopast.BasicLit:
if arg.Kind != goptoken.STRING {
return nil
}
soundName = arg.Value[1 : len(arg.Value)-1] // Unquote the string literal.
case *gopast.Ident:
if argTV.Value != nil {
// If it's a constant, we can get its value.
soundName = argTV.Value.String()
} else {
return nil // TODO: Handle spx auto-binding here.
}
default:
return nil
}
if _, err := s.getSpxSoundResource(soundName); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return []Diagnostic{
{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(fset.Position(callExpr.Args[0].Pos())),
End: FromGopTokenPosition(fset.Position(callExpr.Args[0].End())),
},
Message: fmt.Sprintf("sound resource %q not found", soundName),
},
}
}
return []Diagnostic{
{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(fset.Position(callExpr.Args[0].Pos())),
End: FromGopTokenPosition(fset.Position(callExpr.Args[0].End())),
},
Message: fmt.Sprintf("failed to get sound resource %q: %v", soundName, err),
},
}
}
return nil
}

// validateSpxSpriteAnimateCall validates a spx.Sprite.Animate call.
//
// See https://pkg.go.dev/github.com/goplus/spx#Sprite.Animate
func (s *Server) validateSpxSpriteAnimateCall(typeInfo *goptypesutil.Info, callExpr *gopast.CallExpr, fset *goptoken.FileSet) []Diagnostic {
tv, ok := typeInfo.Types[callExpr.Fun]
if !ok {
return nil
}
sig, ok := tv.Type.(*types.Signature)
if !ok {
return nil
}
recv := sig.Recv()
if recv == nil {
return nil
}
recvType := recv.Type()
if ptr, ok := recvType.(*types.Pointer); ok {
recvType = ptr.Elem()
}
if recvType.String() != "github.com/goplus/spx.Sprite" {
return nil
}

// Extract sprite name from the .spx filename.
spriteName := strings.TrimSuffix(filepath.Base(fset.Position(callExpr.Pos()).Filename), ".spx")
if spriteName == "" || spriteName == "main" {
return nil // This should never happen, but just in case.
}
spriteResource, err := s.getSpxSpriteResource(spriteName)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return []Diagnostic{
{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(fset.Position(callExpr.Fun.Pos())),
End: FromGopTokenPosition(fset.Position(callExpr.Fun.End())),
},
Message: fmt.Sprintf("sprite resource %q not found", spriteName),
},
}
}
return []Diagnostic{
{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(fset.Position(callExpr.Fun.Pos())),
End: FromGopTokenPosition(fset.Position(callExpr.Fun.End())),
},
Message: fmt.Sprintf("failed to get sprite resource %q: %v", spriteName, err),
},
}
}

if len(callExpr.Args) == 0 {
return nil
}
argTV, ok := typeInfo.Types[callExpr.Args[0]]
if !ok {
return nil
}
if !types.Identical(argTV.Type, types.Typ[types.String]) {
return nil
}

var animationName string
switch arg := callExpr.Args[0].(type) {
case *gopast.BasicLit:
if arg.Kind != goptoken.STRING {
return nil
}
animationName = arg.Value[1 : len(arg.Value)-1] // Unquote the string literal.
case *gopast.Ident:
if argTV.Value != nil {
// If it's a constant, we can get its value.
animationName = argTV.Value.String()
} else {
return nil
}
default:
return nil
}
for _, animation := range spriteResource.Animations {
if animation.Name == animationName {
return nil
}
}
return []Diagnostic{
{
Severity: SeverityError,
Range: Range{
Start: FromGopTokenPosition(fset.Position(callExpr.Fun.Pos())),
End: FromGopTokenPosition(fset.Position(callExpr.Fun.End())),
},
Message: fmt.Sprintf("animation %q not found in sprite resource %q", animationName, spriteName),
},
}
}
60 changes: 60 additions & 0 deletions tools/spxls/internal/server/spx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package server

import (
"encoding/json"
"fmt"
"io/fs"
)

// SpxSpriteResource represents a spx sprite resource.
type SpxSpriteResource struct {
Name string `json:"name"`
Costumes []SpxSpriteCostumeResource `json:"costumes"`
CostumeIndex int `json:"costumeIndex"`
Animations []SpxSpriteAnimationResource `json:"fAnimations"`
DefaultAnimation string `json:"defaultAnimation"`
}

// SpxSpriteCostumeResource represents a spx sprite costume resource.
type SpxSpriteCostumeResource struct {
Index int `json:"index"`
Name string `json:"name"`
Path string `json:"path"`
}

// SpxSpriteAnimationResource represents a spx sprite animation resource.
type SpxSpriteAnimationResource struct {
Name string `json:"name"`
}

// getSpxSpriteResource gets a spx sprite resource from the workspace.
func (s *Server) getSpxSpriteResource(name string) (*SpxSpriteResource, error) {
metadata, err := fs.ReadFile(s.workspaceRootFS, fmt.Sprintf("assets/sprites/%s/index.json", name))
if err != nil {
return nil, err
}
sprite := SpxSpriteResource{Name: name}
if err := json.Unmarshal(metadata, &sprite); err != nil {
return nil, err
}
return &sprite, nil
}

// SpxSoundResource represents a sound resource in spx.
type SpxSoundResource struct {
Name string `json:"name"`
Path string `json:"path"`
}

// getSpxSoundResource gets a spx sound resource from the workspace.
func (s *Server) getSpxSoundResource(name string) (*SpxSoundResource, error) {
metadata, err := fs.ReadFile(s.workspaceRootFS, fmt.Sprintf("assets/sounds/%s/index.json", name))
if err != nil {
return nil, err
}
sound := SpxSoundResource{Name: name}
if err := json.Unmarshal(metadata, &sound); err != nil {
return nil, err
}
return &sound, nil
}

0 comments on commit 3a53e0c

Please sign in to comment.