Repository: goplus/gop
Pull request #1529 (2023-11-08 07:06:00)

gop/parser: ParseFSEntry/ParseEntry


Patch:
diff --git a/parser/classfile.go b/parser/classfile.go
new file mode 100644
index 00000000..d4a3e3d8
--- /dev/null
+++ b/parser/classfile.go
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package parser
+
+import (
+	"path/filepath"
+)
+
+// ClassFileExt returns the classfile extension
+func ClassFileExt(path string) (ext string) {
+	ext = filepath.Ext(path)
+	if ext == ".gox" {
+		if c := filepath.Ext(path[:len(path)-4]); c != "" {
+			return c
+		}
+	}
+	return
+}
diff --git a/parser/fsx/fsys.go b/parser/fsx/fsys.go
index 206b2f49..013b16e5 100644
--- a/parser/fsx/fsys.go
+++ b/parser/fsx/fsys.go
@@ -29,6 +29,12 @@ type FileSystem interface {
 	ReadDir(dirname string) ([]fs.DirEntry, error)
 	ReadFile(filename string) ([]byte, error)
 	Join(elem ...string) string
+
+	// Base returns the last element of path.
+	// Trailing path separators are removed before extracting the last element.
+	// If the path is empty, Base returns ".".
+	// If the path consists entirely of separators, Base returns a single separator.
+	Base(filename string) string
 }
 
 // -----------------------------------------------------------------------------
@@ -47,6 +53,14 @@ func (p localFS) Join(elem ...string) string {
 	return filepath.Join(elem...)
 }
 
+// Base returns the last element of path.
+// Trailing path separators are removed before extracting the last element.
+// If the path is empty, Base returns ".".
+// If the path consists entirely of separators, Base returns a single separator.
+func (p localFS) Base(filename string) string {
+	return filepath.Base(filename)
+}
+
 var Local FileSystem = localFS{}
 
 // -----------------------------------------------------------------------------
diff --git a/parser/fsx/memfs/fs.go b/parser/fsx/memfs/fs.go
index 12b692f1..0721f20f 100644
--- a/parser/fsx/memfs/fs.go
+++ b/parser/fsx/memfs/fs.go
@@ -76,6 +76,10 @@ func (p *FileFS) Join(elem ...string) string {
 	return filepath.Join(elem...)
 }
 
+func (p *FileFS) Base(filename string) string {
+	return filepath.Base(filename)
+}
+
 func readSource(src interface{}) ([]byte, error) {
 	switch s := src.(type) {
 	case string:
diff --git a/parser/fsx/memfs/memfs.go b/parser/fsx/memfs/memfs.go
index 82efae4a..4b62ee33 100644
--- a/parser/fsx/memfs/memfs.go
+++ b/parser/fsx/memfs/memfs.go
@@ -109,6 +109,14 @@ func (p *FS) Join(elem ...string) string {
 	return path.Join(elem...)
 }
 
+// Base returns the last element of path.
+// Trailing slashes are removed before extracting the last element.
+// If the path is empty, Base returns ".".
+// If the path consists entirely of slashes, Base returns "/".
+func (p *FS) Base(filename string) string {
+	return path.Base(filename)
+}
+
 // -----------------------------------------------------------------------------
 
 // SingleFile creates a file system that only contains a single file.
diff --git a/parser/parser_gop.go b/parser/parser_gop.go
index 77643083..44adb4a1 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -19,12 +19,10 @@ package parser
 import (
 	"bytes"
 	"errors"
-	"fmt"
 	"io"
 	"io/fs"
 	"os"
 	"path"
-	"path/filepath"
 	"strings"
 
 	goast "go/ast"
@@ -51,6 +49,10 @@ func SetDebug(dbgFlags int) {
 	debugParseError = (dbgFlags & DbgFlagParseError) != 0
 }
 
+var (
+	ErrUnknownFileKind = errors.New("unknown file kind")
+)
+
 type FileSystem = fsx.FileSystem
 
 // -----------------------------------------------------------------------------
@@ -95,15 +97,9 @@ func ParseDirEx(fset *token.FileSet, path string, conf Config) (pkgs map[string]
 	return ParseFSDir(fset, fsx.Local, path, conf)
 }
 
-// ClassFileExt returns the classfile extension
-func ClassFileExt(path string) (ext string) {
-	ext = filepath.Ext(path)
-	if ext == ".gox" {
-		if c := filepath.Ext(path[:len(path)-4]); c != "" {
-			return c
-		}
-	}
-	return
+// ParseEntry calls ParseFSEntry by passing a local filesystem.
+func ParseEntry(fset *token.FileSet, filename string, src interface{}, conf Config) (f *ast.File, err error) {
+	return ParseFSEntry(fset, fsx.Local, filename, src, conf)
 }
 
 // ParseFSDir calls ParseFile for all files with names ending in ".gop" in the
@@ -118,8 +114,8 @@ func ClassFileExt(path string) (ext string) {
 // If the directory couldn't be read, a nil map and the respective error are
 // returned. If a parse error occurred, a non-nil but incomplete map and the
 // first error encountered are returned.
-func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (pkgs map[string]*ast.Package, first error) {
-	list, err := fs.ReadDir(path)
+func ParseFSDir(fset *token.FileSet, fs FileSystem, dir string, conf Config) (pkgs map[string]*ast.Package, first error) {
+	list, err := fs.ReadDir(dir)
 	if err != nil {
 		return nil, err
 	}
@@ -133,7 +129,7 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 		}
 		fname := d.Name()
 		fnameRmGox := fname
-		ext := filepath.Ext(fname)
+		ext := path.Ext(fname)
 		var isProj, isClass, isNormalGox, useGoParser, rmGox bool
 		switch ext {
 		case ".gop":
@@ -145,8 +141,8 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 		case ".gox":
 			isClass = true
 			t := fname[:len(fname)-4]
-			if c := filepath.Ext(t); c != "" {
-				ext, fnameRmGox, rmGox = c, t, true
+			if c := path.Ext(t); c != "" {
+				fnameRmGox, rmGox = t, true
 			} else {
 				isNormalGox = true
 			}
@@ -154,10 +150,11 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 		default:
 			if !isNormalGox {
 				if isProj, isClass = conf.ClassKind(fnameRmGox); !isClass {
-					if rmGox {
-						return nil, fmt.Errorf("not found Go+ class by ext %q for %q", ext, fname)
+					if !rmGox { // unknown fileKind
+						continue
 					}
-					continue
+					// not found Go+ class by ext, but is a .gox file
+					isClass, isNormalGox = true, true
 				}
 			}
 		}
@@ -166,7 +163,7 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 			mode |= ParseGoPlusClass
 		}
 		if !strings.HasPrefix(fname, "_") && (conf.Filter == nil || filter(d, conf.Filter)) {
-			filename := fs.Join(path, fname)
+			filename := fs.Join(dir, fname)
 			if useGoParser {
 				if filedata, err := fs.ReadFile(filename); err == nil {
 					if src, err := goparser.ParseFile(fset, filename, filedata, goparser.Mode(conf.Mode)); err == nil {
@@ -181,11 +178,11 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 				} else {
 					first = err
 				}
-			} else if src, err := ParseFSFile(fset, fs, filename, nil, mode); err == nil {
-				src.IsProj, src.IsClass = isProj, isClass
-				src.IsNormalGox = isNormalGox
-				pkg := reqPkg(pkgs, src.Name.Name)
-				pkg.Files[filename] = src
+			} else if f, err := ParseFSFile(fset, fs, filename, nil, mode); err == nil {
+				f.IsProj, f.IsClass = isProj, isClass
+				f.IsNormalGox = isNormalGox
+				pkg := reqPkg(pkgs, f.Name.Name)
+				pkg.Files[filename] = f
 			} else if first == nil {
 				first = err
 			}
@@ -194,6 +191,50 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 	return
 }
 
+// ParseFSEntry parses the source code of a single Go+ source file and returns the corresponding ast.File node.
+// Compared to ParseFSFile, ParseFSEntry detects fileKind by its filename.
+func ParseFSEntry(fset *token.FileSet, fs FileSystem, filename string, src interface{}, conf Config) (f *ast.File, err error) {
+	fname := fs.Base(filename)
+	fnameRmGox := fname
+	ext := path.Ext(fname)
+	var isProj, isClass, isNormalGox, rmGox bool
+	switch ext {
+	case ".gop", ".go":
+	case ".gox":
+		isClass = true
+		t := fname[:len(fname)-4]
+		if c := path.Ext(t); c != "" {
+			fnameRmGox, rmGox = t, true
+		} else {
+			isNormalGox = true
+		}
+		fallthrough
+	default:
+		if !isNormalGox {
+			if conf.ClassKind == nil {
+				conf.ClassKind = defaultClassKind
+			}
+			if isProj, isClass = conf.ClassKind(fnameRmGox); !isClass {
+				if !rmGox {
+					return nil, ErrUnknownFileKind
+				}
+				// not found Go+ class by ext, but is a .gox file
+				isClass, isNormalGox = true, true
+			}
+		}
+	}
+	mode := conf.Mode
+	if isClass {
+		mode |= ParseGoPlusClass
+	}
+	f, err = ParseFSFile(fset, fs, filename, src, mode)
+	if err == nil {
+		f.IsProj, f.IsClass = isProj, isClass
+		f.IsNormalGox = isNormalGox
+	}
+	return
+}
+
 func filter(d fs.DirEntry, fn func(fs.FileInfo) bool) bool {
 	fi, err := d.Info()
 	return err != nil || fn(fi)


Test patch:
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index a5f398a3..f4da4c60 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -18,7 +18,6 @@ package parser
 
 import (
 	"bytes"
-	"io/ioutil"
 	"os"
 	"path"
 	"reflect"
@@ -113,7 +112,7 @@ func testFrom(t *testing.T, pkgDir, sel string, exclude Mode) {
 		t.Fatal("ParseDir failed:", err, reflect.TypeOf(err), len(pkgs))
 	}
 	for _, pkg := range pkgs {
-		b, err := ioutil.ReadFile(pkgDir + "/parser.expect")
+		b, err := os.ReadFile(pkgDir + "/parser.expect")
 		if err != nil {
 			t.Fatal("Parsing", pkgDir, "-", err)
 		}
@@ -144,6 +143,57 @@ func TestParseGoFiles(t *testing.T) {
 	}
 }
 
+func TestParseEntry(t *testing.T) {
+	fset := token.NewFileSet()
+	src, err := os.ReadFile("./_testdata/functype/functype.go")
+	if err != nil {
+		t.Fatal("os.ReadFile:", err)
+	}
+	conf := Config{}
+	t.Run(".gop file", func(t *testing.T) {
+		f, err := ParseEntry(fset, "./functype.gop", src, conf)
+		if err != nil {
+			t.Fatal("ParseEntry failed:", err)
+		}
+		if f.IsClass || f.IsProj || f.IsNormalGox {
+			t.Fatal("ParseEntry functype.gop:", f.IsClass, f.IsProj, f.IsNormalGox)
+		}
+	})
+	t.Run(".gox file", func(t *testing.T) {
+		f, err := ParseEntry(fset, "./functype.gox", src, conf)
+		if err != nil {
+			t.Fatal("ParseEntry failed:", err)
+		}
+		if !f.IsClass || f.IsProj || !f.IsNormalGox {
+			t.Fatal("ParseEntry functype.gox:", f.IsClass, f.IsProj, f.IsNormalGox)
+		}
+	})
+	t.Run(".foo.gox file", func(t *testing.T) {
+		f, err := ParseEntry(fset, "./functype.foo.gox", src, conf)
+		if err != nil {
+			t.Fatal("ParseEntry failed:", err)
+		}
+		if !f.IsClass || f.IsProj || !f.IsNormalGox {
+			t.Fatal("ParseEntry functype.foo.gox:", f.IsClass, f.IsProj, f.IsNormalGox)
+		}
+	})
+	t.Run(".foo file", func(t *testing.T) {
+		_, err := ParseEntry(fset, "./functype.foo", src, conf)
+		if err != ErrUnknownFileKind {
+			t.Fatal("ParseEntry failed:", err)
+		}
+	})
+	t.Run(".spx file", func(t *testing.T) {
+		f, err := ParseEntry(fset, "./main.spx", src, conf)
+		if err != nil {
+			t.Fatal("ParseEntry failed:", err)
+		}
+		if !f.IsClass || !f.IsProj || f.IsNormalGox {
+			t.Fatal("ParseEntry main.spx:", f.IsClass, f.IsProj, f.IsNormalGox)
+		}
+	})
+}
+
 func TestGopAutoGen(t *testing.T) {
 	fset := token.NewFileSet()
 	fs := memfs.SingleFile("/foo", "gop_autogen.go", ``)
@@ -190,8 +240,8 @@ func TestErrParse(t *testing.T) {
 
 	fs = memfs.SingleFile("/foo", "test.abc.gox", `package foo`)
 	_, err = ParseFSDir(fset, fs, "/foo", Config{})
-	if err == nil {
-		t.Fatal("ParseFSDir test.gop: no error?")
+	if err != nil {
+		t.Fatal("ParseFSDir failed:", err)
 	}
 }
 
@@ -201,7 +251,7 @@ func testFromDir(t *testing.T, sel, relDir string) {
 		t.Fatal("Getwd failed:", err)
 	}
 	dir = path.Join(dir, relDir)
-	fis, err := ioutil.ReadDir(dir)
+	fis, err := os.ReadDir(dir)
 	if err != nil {
 		t.Fatal("ReadDir failed:", err)
 	}