From 253ae7cbfb5636e0db11cf8d21038c71d010c33b Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 15:08:50 +0800 Subject: [PATCH 01/36] feat: reduce calling libclang mutiple time --- _xtool/internal/config/config.go | 74 ++++---- _xtool/internal/config/config_test.go | 169 ++++++++++++++++++ _xtool/internal/config/testdata/hfile/temp1.h | 1 + 3 files changed, 207 insertions(+), 37 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index b212b02f..83b96ff4 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -41,28 +41,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } defer os.Remove(outfile.Name()) - inters := make(map[string]struct{}) - others := []string{} // impl & third - for _, f := range includes { - content := "#include <" + f + ">" - index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ - File: content, - Temp: true, - Args: args, - }) - if err != nil { - panic(err) - } - clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { - if len(incins) == 1 { - filename := filepath.Clean(clang.GoString(inced.FileName())) - info.Inters = append(info.Inters, filename) - inters[filename] = struct{}{} - } - }) - unit.Dispose() - index.Dispose() - } + refMap := make(map[string]int, len(includes)) clangtool.ComposeIncludes(includes, outfile.Name()) index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ @@ -75,41 +54,62 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if err != nil { panic(err) } + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile filename := filepath.Clean(clang.GoString(inced.FileName())) - _, inter := inters[filename] - if len(incins) > 1 && !inter { - others = append(others, filename) + + if len(incins) == 1 { + info.Inters = append(info.Inters, filename) } - }) - if mix { - info.Thirds = others - return info - } + ref, ok := refMap[filename] + if !ok { + refMap[filename] = len(incins) + return + } + // Handle duplicate references: Retain only the reference with the smallest source location. + // Example: + // temp1.h: temp2 tempimpl.h + // temp2.h: temp2 + // The reference count for temp2.h should be 1 (not 2). + // If its count is 2, decrement it to 1. + if len(incins) < ref { + refMap[filename] = len(incins) + } + }) - root, err := filepath.Abs(commonParentDir(info.Inters)) + absLongestPrefix, err := filepath.Abs(CommonParentDir(info.Inters)) if err != nil { panic(err) } - for _, f := range others { - file, err := filepath.Abs(f) + + for filename, ref := range refMap { + if ref == 1 { + continue + } + + if mix { + info.Thirds = append(info.Thirds, filename) + continue + } + filePath, err := filepath.Abs(filename) if err != nil { panic(err) } - if strings.HasPrefix(file, root) { - info.Impls = append(info.Impls, f) + if strings.HasPrefix(filePath, absLongestPrefix) { + info.Impls = append(info.Impls, filename) } else { - info.Thirds = append(info.Thirds, f) + info.Thirds = append(info.Thirds, filename) } } + return info } // commonParentDir finds the longest common parent directory path for a given slice of paths. // For example, given paths ["/a/b/c/d", "/a/b/e/f"], it returns "/a/b". -func commonParentDir(paths []string) string { +func CommonParentDir(paths []string) string { if len(paths) == 0 { return "" } diff --git a/_xtool/internal/config/config_test.go b/_xtool/internal/config/config_test.go index 69fe87bf..2009e8d2 100644 --- a/_xtool/internal/config/config_test.go +++ b/_xtool/internal/config/config_test.go @@ -2,11 +2,16 @@ package config_test import ( "fmt" + "os" "path/filepath" "reflect" "strings" "testing" + "time" + "github.com/goplus/lib/c/clang" + clangutils "github.com/goplus/llcppg/_xtool/internal/clang" + "github.com/goplus/llcppg/_xtool/internal/clangtool" "github.com/goplus/llcppg/_xtool/internal/config" llconfig "github.com/goplus/llcppg/config" ) @@ -73,3 +78,167 @@ func TestPkgHfileInfo(t *testing.T) { }) } } + +func TestLongestPrefix(t *testing.T) { + testCases := []struct { + name string + strs []string + want string + }{ + { + name: "empty string 1", + strs: []string{}, + want: "", + }, + { + name: "empty string 2", + strs: []string{"", ""}, + want: ".", + }, + { + name: "one empty string(b)", + strs: []string{"/a", ""}, + want: "", + }, + { + name: "one empty string(a)", + strs: []string{"", "/a"}, + + want: "", + }, + // FIXME: substring bug + // { + // name: "b is substring of a", + // strs: []string{"/usr/a/b", "/usr/a"}, + // want: "/usr/a", + // }, + // { + // name: "a is substring of b", + // strs: []string{"/usr/c", "/usr/c/b"}, + // want: "/usr/c", + // }, + { + name: "normal case 1", + strs: []string{"testdata/hfile/temp1.h", "testdata/thirdhfile/third.h"}, + want: "testdata", + }, + { + name: "normal case 2", + strs: []string{"testdata/hfile/temp1.h", "testdata/hfile/third.h"}, + + want: "testdata/hfile", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := config.CommonParentDir(tc.strs); got != tc.want { + t.Fatalf("unexpected longest prefix: want %s got %s", tc.want, got) + } + }) + } +} + +func benchmarkFn(fn func()) time.Duration { + now := time.Now() + + fn() + + return time.Since(now) +} + +func BenchmarkPkgHfileInfo(t *testing.B) { + include := []string{"temp1.h", "temp2.h"} + cflags := []string{"-I./testdata/hfile", "-I./testdata/thirdhfile"} + t1 := benchmarkFn(func() { + for i := 0; i < t.N; i++ { + pkgHfileInfo(include, cflags, false) + } + }) + + t2 := benchmarkFn(func() { + for i := 0; i < t.N; i++ { + config.PkgHfileInfo(include, cflags, false) + } + }) + + fmt.Println("old PkgHfileInfo elapsed: ", t1, "new PkgHfileInfo elasped: ", t2) +} + +func pkgHfileInfo(includes []string, args []string, mix bool) *config.PkgHfilesInfo { + info := &config.PkgHfilesInfo{ + Inters: []string{}, + Impls: []string{}, + Thirds: []string{}, + } + outfile, err := os.CreateTemp("", "compose_*.h") + if err != nil { + panic(err) + } + defer os.Remove(outfile.Name()) + + inters := make(map[string]struct{}) + others := []string{} // impl & third + for _, f := range includes { + content := "#include <" + f + ">" + index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ + File: content, + Temp: true, + Args: args, + }) + if err != nil { + panic(err) + } + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { + if len(incins) == 1 { + filename := filepath.Clean(clang.GoString(inced.FileName())) + info.Inters = append(info.Inters, filename) + inters[filename] = struct{}{} + } + }) + unit.Dispose() + index.Dispose() + } + + clangtool.ComposeIncludes(includes, outfile.Name()) + index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ + File: outfile.Name(), + Temp: false, + Args: args, + }) + defer unit.Dispose() + defer index.Dispose() + if err != nil { + panic(err) + } + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { + // not in the first level include maybe impl or third hfile + filename := filepath.Clean(clang.GoString(inced.FileName())) + _, inter := inters[filename] + if len(incins) > 1 && !inter { + others = append(others, filename) + } + }) + + if mix { + info.Thirds = others + return info + } + + root, err := filepath.Abs(config.CommonParentDir(info.Inters)) + if err != nil { + panic(err) + } + for _, f := range others { + file, err := filepath.Abs(f) + if err != nil { + panic(err) + } + if strings.HasPrefix(file, root) { + info.Impls = append(info.Impls, f) + } else { + info.Thirds = append(info.Thirds, f) + } + } + return info +} diff --git a/_xtool/internal/config/testdata/hfile/temp1.h b/_xtool/internal/config/testdata/hfile/temp1.h index 704cd55e..14a2a764 100644 --- a/_xtool/internal/config/testdata/hfile/temp1.h +++ b/_xtool/internal/config/testdata/hfile/temp1.h @@ -1,2 +1,3 @@ #include "tempimpl.h" +#include "temp2.h" #include \ No newline at end of file From 19846fb7cd6142ac20f92ad38da96f3edcd68e99 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 15:10:17 +0800 Subject: [PATCH 02/36] test: add commondir test case 3 --- _xtool/internal/config/config_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/_xtool/internal/config/config_test.go b/_xtool/internal/config/config_test.go index 2009e8d2..b6924579 100644 --- a/_xtool/internal/config/config_test.go +++ b/_xtool/internal/config/config_test.go @@ -128,6 +128,11 @@ func TestLongestPrefix(t *testing.T) { want: "testdata/hfile", }, + { + name: "normal case 3", + strs: []string{"/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h"}, + want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + }, } for _, tc := range testCases { From bcc3e2e174712ee99fafaecc7f74652124cd48e0 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 15:13:53 +0800 Subject: [PATCH 03/36] test: remove test case 3 --- _xtool/internal/config/config_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/_xtool/internal/config/config_test.go b/_xtool/internal/config/config_test.go index b6924579..78581f43 100644 --- a/_xtool/internal/config/config_test.go +++ b/_xtool/internal/config/config_test.go @@ -128,11 +128,12 @@ func TestLongestPrefix(t *testing.T) { want: "testdata/hfile", }, - { - name: "normal case 3", - strs: []string{"/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h"}, - want: "/opt/homebrew/Cellar/cjson/1.7.18/include", - }, + // FIXME: absolute path + // { + // name: "normal case 3", + // strs: []string{"/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h"}, + // want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + // }, } for _, tc := range testCases { From ec3dfe83964e53e05a931309d5770f72ea391a8d Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 18:23:43 +0800 Subject: [PATCH 04/36] fix: use MM to retrieve interface --- _xtool/internal/config/config.go | 65 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 83b96ff4..5fb73dc6 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -1,8 +1,12 @@ package config import ( + "bufio" + "maps" "os" "path/filepath" + "slices" + "sort" "strings" "github.com/goplus/lib/c/clang" @@ -41,54 +45,49 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } defer os.Remove(outfile.Name()) - refMap := make(map[string]int, len(includes)) + mmOutput, err := os.CreateTemp("", "mmoutput_*") + if err != nil { + panic(err) + } + defer os.Remove(mmOutput.Name()) clangtool.ComposeIncludes(includes, outfile.Name()) index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: args, + Args: append(args, "-MMD", "-MF", mmOutput.Name()), }) + defer unit.Dispose() defer index.Dispose() if err != nil { panic(err) } + inters := ParseMMOutout(outfile.Name(), mmOutput) + var others []string + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile filename := filepath.Clean(clang.GoString(inced.FileName())) - if len(incins) == 1 { - info.Inters = append(info.Inters, filename) - } - - ref, ok := refMap[filename] - if !ok { - refMap[filename] = len(incins) + // skip the composed header + if filename == outfile.Name() { return } - // Handle duplicate references: Retain only the reference with the smallest source location. - // Example: - // temp1.h: temp2 tempimpl.h - // temp2.h: temp2 - // The reference count for temp2.h should be 1 (not 2). - // If its count is 2, decrement it to 1. - if len(incins) < ref { - refMap[filename] = len(incins) + if _, ok := inters[filename]; !ok { + others = append(others, filename) } }) + info.Inters = slices.Collect(maps.Keys(inters)) + absLongestPrefix, err := filepath.Abs(CommonParentDir(info.Inters)) if err != nil { panic(err) } - for filename, ref := range refMap { - if ref == 1 { - continue - } - + for _, filename := range others { if mix { info.Thirds = append(info.Thirds, filename) continue @@ -104,6 +103,9 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } } + sort.Strings(info.Inters) + sort.Strings(info.Impls) + return info } @@ -128,3 +130,22 @@ func CommonParentDir(paths []string) string { } return filepath.Dir(paths[0]) } + +func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters map[string]struct{}) { + scanner := bufio.NewScanner(outputFile) + + fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") + + inters = make(map[string]struct{}) + + for scanner.Scan() { + // skip composed header file + if strings.Contains(scanner.Text(), fileName) { + continue + } + inter := filepath.Clean(strings.TrimSpace(strings.TrimSuffix(scanner.Text(), `\`))) + inters[inter] = struct{}{} + } + + return +} From 1a3a4f43c09ddec715c84d67129a1b78f946690a Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 18:52:40 +0800 Subject: [PATCH 05/36] fix: mm output parse --- _xtool/internal/config/config.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 5fb73dc6..d1011961 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "bufio" + "fmt" "maps" "os" "path/filepath" @@ -78,6 +79,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if _, ok := inters[filename]; !ok { others = append(others, filename) } + fmt.Fprintln(os.Stderr, "tttttt", filename, inters) }) info.Inters = slices.Collect(maps.Keys(inters)) @@ -143,8 +145,10 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m if strings.Contains(scanner.Text(), fileName) { continue } - inter := filepath.Clean(strings.TrimSpace(strings.TrimSuffix(scanner.Text(), `\`))) - inters[inter] = struct{}{} + for _, elem := range strings.Fields(scanner.Text()) { + inter := filepath.Clean(strings.TrimSpace(strings.TrimSuffix(elem, `\`))) + inters[inter] = struct{}{} + } } return From 4c9ccfaf84b1217f9f8fe96b6bcbf971d32f8a48 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 18:57:33 +0800 Subject: [PATCH 06/36] fix: mm output parse --- _xtool/internal/config/config.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index d1011961..3d934251 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -1,8 +1,7 @@ package config import ( - "bufio" - "fmt" + "io" "maps" "os" "path/filepath" @@ -79,7 +78,6 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if _, ok := inters[filename]; !ok { others = append(others, filename) } - fmt.Fprintln(os.Stderr, "tttttt", filename, inters) }) info.Inters = slices.Collect(maps.Keys(inters)) @@ -134,21 +132,19 @@ func CommonParentDir(paths []string) string { } func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters map[string]struct{}) { - scanner := bufio.NewScanner(outputFile) - fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") inters = make(map[string]struct{}) - for scanner.Scan() { + content, _ := io.ReadAll(outputFile) + + for _, line := range strings.Fields(string(content)) { // skip composed header file - if strings.Contains(scanner.Text(), fileName) { + if strings.Contains(line, fileName) || line == `\` { continue } - for _, elem := range strings.Fields(scanner.Text()) { - inter := filepath.Clean(strings.TrimSpace(strings.TrimSuffix(elem, `\`))) - inters[inter] = struct{}{} - } + inter := filepath.Clean(line) + inters[inter] = struct{}{} } return From 277bc64225f588b30e5bf22f918cef136f68409c Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:01:28 +0800 Subject: [PATCH 07/36] add test --- _xtool/internal/config/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 3d934251..3f9cb856 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "io" "maps" "os" @@ -78,6 +79,8 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if _, ok := inters[filename]; !ok { others = append(others, filename) } + + fmt.Fprintln(os.Stderr, "fffffff", filename, inters) }) info.Inters = slices.Collect(maps.Keys(inters)) From f22a90d46b899e9ef441b49466b0dd8b6575e1bb Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:04:47 +0800 Subject: [PATCH 08/36] add test --- _xtool/internal/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 3f9cb856..127ffa9e 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -146,6 +146,8 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m if strings.Contains(line, fileName) || line == `\` { continue } + fmt.Fprintln(line) + inter := filepath.Clean(line) inters[inter] = struct{}{} } From 2f4085b456186595298ba76625973364729ab649 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:06:10 +0800 Subject: [PATCH 09/36] add test --- _xtool/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 127ffa9e..bf6420d1 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -146,7 +146,7 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m if strings.Contains(line, fileName) || line == `\` { continue } - fmt.Fprintln(line) + fmt.Fprintln(os.Stderr, "aaaaa", line) inter := filepath.Clean(line) inters[inter] = struct{}{} From 8b8b9ebd9178d34f13dd7b6153977ab5c507dfd9 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:09:43 +0800 Subject: [PATCH 10/36] add test --- _xtool/internal/config/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index bf6420d1..0cbe81ec 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -141,12 +141,13 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m content, _ := io.ReadAll(outputFile) + fmt.Fprintln(os.Stderr, "aaaaa", string(content)) + for _, line := range strings.Fields(string(content)) { // skip composed header file if strings.Contains(line, fileName) || line == `\` { continue } - fmt.Fprintln(os.Stderr, "aaaaa", line) inter := filepath.Clean(line) inters[inter] = struct{}{} From 39ee6defa0c97aab07c15bd1fa893c0e5def15df Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:15:03 +0800 Subject: [PATCH 11/36] add test --- _xtool/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 0cbe81ec..e5ae31a1 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -56,7 +56,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(args, "-MMD", "-MF", mmOutput.Name()), + Args: append(args, "-MD", "-MF", mmOutput.Name()), }) defer unit.Dispose() From 0494289694b283bbbc10b72e0474d1ff327c8f67 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 19:20:56 +0800 Subject: [PATCH 12/36] add test --- _xtool/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index e5ae31a1..c124e710 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -56,7 +56,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(args, "-MD", "-MF", mmOutput.Name()), + Args: append(args, "-MM", "-MF", mmOutput.Name()), }) defer unit.Dispose() From aa853a58ea4f932009062de032a1cf4edfa80618 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:09:13 +0800 Subject: [PATCH 13/36] add test --- _xtool/internal/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index c124e710..0d7af220 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -56,7 +56,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(args, "-MM", "-MF", mmOutput.Name()), + Args: append(args, "-fkeep-system-includes", "-MD", "-MF", mmOutput.Name()), }) defer unit.Dispose() @@ -71,7 +71,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile filename := filepath.Clean(clang.GoString(inced.FileName())) - + inced. // skip the composed header if filename == outfile.Name() { return From 2658889664f747a5e432a82383f5c6c2a5cc30f0 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:09:37 +0800 Subject: [PATCH 14/36] add test --- _xtool/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 0d7af220..d235d4bc 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -71,7 +71,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile filename := filepath.Clean(clang.GoString(inced.FileName())) - inced. + // skip the composed header if filename == outfile.Name() { return From 8ae30772e5ca6aeae7d6fd7d50b7a82d17485fea Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:26:12 +0800 Subject: [PATCH 15/36] add test --- _xtool/internal/config/config.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index d235d4bc..5aec9b36 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -56,7 +56,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(args, "-fkeep-system-includes", "-MD", "-MF", mmOutput.Name()), + Args: append(ignoreIncludesArgs(includes), "-MD", "-MF", mmOutput.Name()), }) defer unit.Dispose() @@ -155,3 +155,9 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m return } + +func ignoreIncludesArgs(includes []string) (args []string) { + for _, inc := range includes { + args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc)) + } +} From fbcdd58d70dd83ba9de4fe6f76ca148ba83b8e70 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:27:34 +0800 Subject: [PATCH 16/36] add test --- _xtool/internal/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 5aec9b36..3a2b5878 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -160,4 +160,5 @@ func ignoreIncludesArgs(includes []string) (args []string) { for _, inc := range includes { args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc)) } + return } From 726df0c306429d28607170ee4119d7ad79d33567 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:28:12 +0800 Subject: [PATCH 17/36] add test --- _xtool/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 3a2b5878..878ed59e 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -56,7 +56,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(ignoreIncludesArgs(includes), "-MD", "-MF", mmOutput.Name()), + Args: append(ignoreIncludesArgs(includes), "-MMD", "-MF", mmOutput.Name()), }) defer unit.Dispose() From 11003943e5426e0dcd363d2183f9447802136969 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:35:18 +0800 Subject: [PATCH 18/36] add test --- _xtool/internal/config/config.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 878ed59e..91e78957 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -52,11 +52,15 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } defer os.Remove(mmOutput.Name()) + args = append(args, ignoreIncludesArgs(includes)...) + + args = append(args, "-MM", "-MF", mmOutput.Name()) + clangtool.ComposeIncludes(includes, outfile.Name()) index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, - Args: append(ignoreIncludesArgs(includes), "-MMD", "-MF", mmOutput.Name()), + Args: args, }) defer unit.Dispose() From 7844d98558d9574184bca79776c02d27009e6bc7 Mon Sep 17 00:00:00 2001 From: Haolan Date: Thu, 5 Jun 2025 20:49:29 +0800 Subject: [PATCH 19/36] add test --- _xtool/internal/config/config.go | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/_xtool/internal/config/config.go b/_xtool/internal/config/config.go index 91e78957..547d8be0 100644 --- a/_xtool/internal/config/config.go +++ b/_xtool/internal/config/config.go @@ -69,7 +69,8 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { panic(err) } - inters := ParseMMOutout(outfile.Name(), mmOutput) + inters := make(map[string]struct{}) + includeMap := ParseMMOutout(outfile.Name(), mmOutput) var others []string clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { @@ -80,11 +81,23 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if filename == outfile.Name() { return } - if _, ok := inters[filename]; !ok { - others = append(others, filename) + refcnt := len(incins) + for _, inc := range incins { + incFileName := clang.GoString(inc.File().FileName()) + + if incFileName == outfile.Name() { + refcnt-- + continue + } + if _, ok := includeMap[incFileName]; ok { + refcnt-- + } } - fmt.Fprintln(os.Stderr, "fffffff", filename, inters) + if refcnt == 0 { + inters[filename] = struct{}{} + } + others = append(others, filename) }) info.Inters = slices.Collect(maps.Keys(inters)) @@ -95,6 +108,9 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } for _, filename := range others { + if _, isInterface := inters[filename]; isInterface { + continue + } if mix { info.Thirds = append(info.Thirds, filename) continue @@ -138,10 +154,10 @@ func CommonParentDir(paths []string) string { return filepath.Dir(paths[0]) } -func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters map[string]struct{}) { +func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (includeMap map[string]struct{}) { fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") - inters = make(map[string]struct{}) + includeMap = make(map[string]struct{}) content, _ := io.ReadAll(outputFile) @@ -153,8 +169,7 @@ func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (inters m continue } - inter := filepath.Clean(line) - inters[inter] = struct{}{} + includeMap[filepath.Clean(line)] = struct{}{} } return From ba356a30e9c245b44d0a4eddc18aff333dd74d16 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 13:48:48 +0800 Subject: [PATCH 20/36] feat: implement trie for longest common prefix --- _xtool/internal/config/trie.go | 187 +++++++++++++ _xtool/internal/config/trie_test.go | 413 ++++++++++++++++++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 _xtool/internal/config/trie.go create mode 100644 _xtool/internal/config/trie_test.go diff --git a/_xtool/internal/config/trie.go b/_xtool/internal/config/trie.go new file mode 100644 index 00000000..1641c2ba --- /dev/null +++ b/_xtool/internal/config/trie.go @@ -0,0 +1,187 @@ +package config + +import ( + "iter" + "os" + "path/filepath" + "slices" + "strings" +) + +type Segmenter func(s string) iter.Seq[string] + +type TrieNode struct { + isLeaf bool // Indicates if this node represents the end of a word + linkCount int // Number of children nodes + children map[string]*TrieNode // Map of child nodes by segment +} + +// Creates a new TrieNode with empty children map +func NewTrieNode() *TrieNode { + return &TrieNode{children: make(map[string]*TrieNode)} +} + +type Trie struct { + root *TrieNode // Root node of the trie + segmenter Segmenter // Function to split strings into segments +} +type Options func(*Trie) // Function type for configuring Trie options + +func skipEmpty(s []string) []string { + for len(s) > 0 && s[0] == "" { + s = s[1:] + } + return s +} + +func splitPathAbsSafe(path string) (paths []string) { + originalPath := filepath.Clean(path) + + sep := string(os.PathSeparator) + + // keep absolute path info + if filepath.IsAbs(originalPath) { + i := strings.Index(originalPath[1:], sep) + if i > 0 { + // bound edge: if i is greater than zero, which means there's second separator + // for example, /usr/, i: 3, with first separator what we just skipped, i: 4 + paths = append(paths, originalPath[0:i+1]) + paths = append(paths, skipEmpty(strings.Split(originalPath[i+1:], sep))...) + } else { + // start with / but no other / is found, like /usr + paths = append(paths, originalPath) + } + } + + if len(paths) == 0 { + paths = skipEmpty(strings.Split(originalPath, sep)) + } + + return +} + +// Returns an option to configure path segmenter +// Splits strings by OS path separator and yields each segment +func WithPathSegmenter() Options { + return func(t *Trie) { + t.segmenter = func(s string) iter.Seq[string] { + return func(yield func(string) bool) { + for _, path := range splitPathAbsSafe(s) { + if path != "" && !yield(path) { + return + } + } + } + } + } +} + +// Returns an option to configure reverse path segmenter +// Splits and reverses strings by OS path separator +func WithReversePathSegmenter() Options { + return func(t *Trie) { + t.segmenter = func(s string) iter.Seq[string] { + return func(yield func(string) bool) { + paths := splitPathAbsSafe(s) + + slices.Reverse(paths) + + for _, path := range paths { + if path != "" && !yield(path) { + return + } + } + } + } + } +} + +// Creates a new Trie with default path segmenter +// Applies all provided options to configure the Trie +func NewTrie(opts ...Options) *Trie { + t := &Trie{root: NewTrieNode()} + + WithPathSegmenter()(t) + + for _, o := range opts { + o(t) + } + + return t +} + +// Inserts a string into the trie +// Creates nodes for each segment in the string +func (t *Trie) Insert(s string) { + if s == "" { + return + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + child = NewTrieNode() + node.children[segment] = child + node.linkCount++ + } + node = child + } + node.isLeaf = true +} + +// Searches for a prefix in the trie +// Returns the node at the end of the prefix or nil if not found +func (t *Trie) searchPrefix(s string) *TrieNode { + if s == "" { + return nil + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + return nil + } + node = child + } + + return node +} + +// Finds the longest common prefix of the given string +// Returns the longest prefix that exists in the trie +// +// Implement Source: https://leetcode.com/problems/longest-common-prefix/solutions/127449/longest-common-prefix +func (t *Trie) LongestPrefix(s string) string { + var prefix []string + + node := t.root + + for segment := range t.segmenter(s) { + child := node.children[segment] + + isLongestPrefix := child != nil && node.linkCount == 1 && !node.isLeaf + + if !isLongestPrefix { + break + } + + prefix = append(prefix, segment) + node = child + } + + return filepath.Join(prefix...) +} + +// Checks if the trie contains the given string as a prefix +func (t *Trie) Contains(s string) bool { + return t.searchPrefix(s) != nil +} + +// Checks if the trie contains the exact string +// Returns true if the string exists in the trie +func (t *Trie) Search(s string) bool { + node := t.searchPrefix(s) + return node != nil && node.isLeaf +} diff --git a/_xtool/internal/config/trie_test.go b/_xtool/internal/config/trie_test.go new file mode 100644 index 00000000..cc2ea964 --- /dev/null +++ b/_xtool/internal/config/trie_test.go @@ -0,0 +1,413 @@ +package config_test + +import ( + "testing" + + "github.com/goplus/llcppg/_xtool/internal/config" +) + +func TestTrieContains(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "empty string", + search: "abc", + want: false, + }, + { + name: "input empty string", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "one string", + search: "/a", + inserted: []string{"/a"}, + want: true, + }, + { + name: "two string", + search: "/a", + inserted: []string{"/a", "/b"}, + want: true, + }, + { + name: "multiple string case 1", + search: "/c", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 2", + search: "", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 3", + search: "/c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: true, + }, + + { + name: "multiple string case 4", + search: "/c/d", + inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, + want: true, + }, + { + name: "substring string case 1", + search: "/a/b", + inserted: []string{"/a"}, + want: false, + }, + { + name: "substring string case 2", + search: "/a", + inserted: []string{"/a/b"}, + want: true, + }, + + { + name: "substring string case 3", + search: "/a/b", + inserted: []string{"/a/b", "/a/b/c"}, + want: true, + }, + + { + name: "absolute path case 1", + search: "a", + inserted: []string{"/a/b"}, + want: false, + }, + { + name: "substring string case 2", + search: "/a", + inserted: []string{"a/b", "a/b/c"}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := config.NewTrie() + + for _, i := range tc.inserted { + trie.Insert(i) + } + if got := trie.Contains(tc.search); got != tc.want { + t.Fatalf("unexpected result: want %v got %v", tc.want, got) + } + }) + } +} + +func TestTrieSearch(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "Empty string insertion and search", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "Single directory exact match", + search: "/usr/local/bin/", + inserted: []string{"/usr/local/bin/"}, + want: true, + }, + { + name: "Single directory partial match", + search: "/usr/local/bin/python", + inserted: []string{"/usr/local/bin/"}, + want: false, + }, + { + name: "Multiple directories exact match", + search: "/usr/local/lib/", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, + want: true, + }, + { + name: "Multiple directories partial match", + search: "/usr/local/lib/python", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, + want: false, + }, + { + name: "Mixed path separators", + search: "/usr/local/bin/", + inserted: []string{"/usr/local/bin/"}, + want: true, + }, + { + name: "Non-existent path", + search: "/non/existent/path", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/"}, + want: false, + }, + { + name: "Empty search string", + search: "", + inserted: []string{"/usr/local/bin/"}, + want: false, + }, + { + name: "Subdirectory search", + search: "/usr/local/bin/", + inserted: []string{"/usr/local/bin/"}, + want: true, + }, + { + name: "Deep directory structure", + search: "/a/b/c/d/e/f/g", + inserted: []string{"/a/b/c/d/e/f/g"}, + want: true, + }, + { + name: "Long path with special characters", + search: "/home/user/!@#$%^&*()", + inserted: []string{"/home/user/!@#$%^&*()"}, + want: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := config.NewTrie() + for _, word := range tc.inserted { + trie.Insert(word) + } + if got := trie.Search(tc.search); got != tc.want { + t.Fatalf("Search(%q) = %v, want %v", tc.search, got, tc.want) + } + }) + } +} + +func TestTrieLongestPrefix(t *testing.T) { + tests := []struct { + name string + inserted []string + input string + want string + }{ + { + name: "Empty trie", + inserted: []string{}, + input: "/usr/local/bin", + want: "", + }, + { + name: "Single directory exact match", + inserted: []string{"/usr/local/bin/"}, + input: "/usr/local/bin", + want: "/usr/local/bin", + }, + { + name: "Single directory partial match", + inserted: []string{"/usr/local/bin/"}, + input: "/usr/local/bin/python", + want: "/usr/local/bin", + }, + { + name: "Multiple directories with common prefix", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, + input: "/usr/local/bin/python", + want: "/usr", + }, + { + name: "No common prefix", + inserted: []string{"/home/user/", "/var/log/", "/tmp/"}, + input: "/etc/passwd", + want: "", + }, + { + name: "Reverse path match", + inserted: []string{"bin", "lib", "include"}, + input: "include/lib/bin", + want: "", + }, + { + name: "Longer input than stored", + inserted: []string{"/short/"}, + input: "/shorter/path", + want: "", + }, + { + name: "Empty input", + inserted: []string{"/test/"}, + input: "", + want: "", + }, + { + name: "No match", + inserted: []string{"/apple/", "/banana/"}, + input: "/cherry/", + want: "", + }, + { + name: "Partial reverse match", + inserted: []string{"bin", "lib", "include"}, + input: "lib/bin", + want: "", + }, + { + name: "normal case 1", + inserted: []string{ + "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", + "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h", + }, + input: "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", + want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + trie := config.NewTrie() + for _, word := range tt.inserted { + trie.Insert(word) + } + result := trie.LongestPrefix(tt.input) + if result != tt.want { + t.Errorf("LongestPrefix(%q) = %q, want %q", tt.input, result, tt.want) + } + }) + } +} + +func TestTrieReverse(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "empty string", + search: "abc", + want: false, + }, + { + name: "input empty string", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "one string", + search: "/a", + inserted: []string{"/a"}, + want: true, + }, + { + name: "two string", + search: "/a", + inserted: []string{"/a", "/b"}, + want: true, + }, + { + name: "multiple string case 1", + search: "/c", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 2", + search: "", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 3", + search: "/c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: false, + }, + + { + name: "multiple string case 4", + search: "/c/d", + inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, + want: false, + }, + + { + name: "multiple string case 5", + search: "c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: true, + }, + { + name: "substring string case 1", + search: "/a/b", + inserted: []string{"/a"}, + want: false, + }, + { + name: "substring string case 2", + search: "b", + inserted: []string{"/a/b"}, + want: true, + }, + + { + name: "substring string case 3", + search: "/a/b", + inserted: []string{"/a/b", "/a/b/c"}, + want: true, + }, + + { + name: "normal case 1", + search: "libxslt/variables.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: true, + }, + + { + name: "normal case 2", + search: "libxslt/c14n.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := config.NewTrie(config.WithReversePathSegmenter()) + + for _, i := range tc.inserted { + trie.Insert(i) + } + if got := trie.Contains(tc.search); got != tc.want { + t.Fatalf("unexpected result: want %v got %v", tc.want, got) + } + }) + } +} From 9cac662d83b554d8d22c6f839c8069b9d11f2406 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 13:59:47 +0800 Subject: [PATCH 21/36] test: add more abs tests --- _xtool/internal/config/trie.go | 5 ++++- _xtool/internal/config/trie_test.go | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/_xtool/internal/config/trie.go b/_xtool/internal/config/trie.go index 1641c2ba..dff45380 100644 --- a/_xtool/internal/config/trie.go +++ b/_xtool/internal/config/trie.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "iter" "os" "path/filepath" @@ -57,6 +58,8 @@ func splitPathAbsSafe(path string) (paths []string) { paths = skipEmpty(strings.Split(originalPath, sep)) } + fmt.Println(paths) + return } @@ -162,7 +165,7 @@ func (t *Trie) LongestPrefix(s string) string { child := node.children[segment] isLongestPrefix := child != nil && node.linkCount == 1 && !node.isLeaf - + fmt.Println(segment, child, node) if !isLongestPrefix { break } diff --git a/_xtool/internal/config/trie_test.go b/_xtool/internal/config/trie_test.go index cc2ea964..faa87672 100644 --- a/_xtool/internal/config/trie_test.go +++ b/_xtool/internal/config/trie_test.go @@ -90,7 +90,7 @@ func TestTrieContains(t *testing.T) { want: false, }, { - name: "substring string case 2", + name: "absolute path case 2", search: "/a", inserted: []string{"a/b", "a/b/c"}, want: false, @@ -275,6 +275,18 @@ func TestTrieLongestPrefix(t *testing.T) { input: "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", want: "/opt/homebrew/Cellar/cjson/1.7.18/include", }, + { + name: "absolute path case 1", + inserted: []string{"/usr", "usr", "/usr/include"}, + input: "/usr", + want: "", + }, + { + name: "absolute path case 2", + inserted: []string{"usr/share", "/usr", "usr/include"}, + input: "usr/include/share", + want: "usr", + }, } for _, tt := range tests { From e25406b219164513d7aff71c2a71d7a560574ffd Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 14:04:20 +0800 Subject: [PATCH 22/36] chore: remove println --- _xtool/internal/config/trie.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/_xtool/internal/config/trie.go b/_xtool/internal/config/trie.go index dff45380..1641c2ba 100644 --- a/_xtool/internal/config/trie.go +++ b/_xtool/internal/config/trie.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "iter" "os" "path/filepath" @@ -58,8 +57,6 @@ func splitPathAbsSafe(path string) (paths []string) { paths = skipEmpty(strings.Split(originalPath, sep)) } - fmt.Println(paths) - return } @@ -165,7 +162,7 @@ func (t *Trie) LongestPrefix(s string) string { child := node.children[segment] isLongestPrefix := child != nil && node.linkCount == 1 && !node.isLeaf - fmt.Println(segment, child, node) + if !isLongestPrefix { break } From 1c1c16f98e93f910a28a37c5d84503cd572da238 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 14:14:00 +0800 Subject: [PATCH 23/36] test: fix test --- _xtool/internal/config/trie_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/config/trie_test.go b/_xtool/internal/config/trie_test.go index faa87672..3386cb05 100644 --- a/_xtool/internal/config/trie_test.go +++ b/_xtool/internal/config/trie_test.go @@ -285,7 +285,7 @@ func TestTrieLongestPrefix(t *testing.T) { name: "absolute path case 2", inserted: []string{"usr/share", "/usr", "usr/include"}, input: "usr/include/share", - want: "usr", + want: "", }, } From c8484dedaa19abfcf711f040c8842269f8872404 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 15:25:51 +0800 Subject: [PATCH 24/36] chore: fix namespace --- _xtool/internal/header/trie_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index 331fff97..40c0b958 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -3,7 +3,7 @@ package header_test import ( "testing" - "github.com/goplus/llcppg/_xtool/internal/config" + "github.com/goplus/llcppg/_xtool/internal/header" ) func TestTrieContains(t *testing.T) { @@ -99,7 +99,7 @@ func TestTrieContains(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - trie := config.NewTrie() + trie := header.NewTrie() for _, i := range tc.inserted { trie.Insert(i) @@ -188,7 +188,7 @@ func TestTrieSearch(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - trie := config.NewTrie() + trie := header.NewTrie() for _, word := range tc.inserted { trie.Insert(word) } @@ -291,7 +291,7 @@ func TestTrieLongestPrefix(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - trie := config.NewTrie() + trie := header.NewTrie() for _, word := range tt.inserted { trie.Insert(word) } @@ -412,7 +412,7 @@ func TestTrieReverse(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - trie := config.NewTrie(config.WithReversePathSegmenter()) + trie := header.NewTrie(header.WithReversePathSegmenter()) for _, i := range tc.inserted { trie.Insert(i) From e6de029ac758c67298afdbd31eca54a7714bd79e Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 15:59:47 +0800 Subject: [PATCH 25/36] fix: change contains logic --- _xtool/internal/header/trie.go | 18 +++++++++++++++++- _xtool/internal/header/trie_test.go | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go index 9ea8a318..7469588e 100644 --- a/_xtool/internal/header/trie.go +++ b/_xtool/internal/header/trie.go @@ -176,7 +176,23 @@ func (t *Trie) LongestPrefix(s string) string { // Checks if the trie contains the given string as a prefix func (t *Trie) Contains(s string) bool { - return t.searchPrefix(s) != nil + if s == "" { + return false + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + if node == t.root { + node = nil + } + break + } + node = child + } + + return node != nil } // Checks if the trie contains the exact string diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index 40c0b958..0c60b43a 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -67,7 +67,7 @@ func TestTrieContains(t *testing.T) { name: "substring string case 1", search: "/a/b", inserted: []string{"/a"}, - want: false, + want: true, }, { name: "substring string case 2", @@ -358,7 +358,7 @@ func TestTrieReverse(t *testing.T) { name: "multiple string case 4", search: "/c/d", inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, - want: false, + want: true, }, { From cc7525494613cd5f46b754329a4af27589f9335c Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 16:01:59 +0800 Subject: [PATCH 26/36] fix: use trie to filter out non-interface header files --- _xtool/internal/header/header.go | 55 ++++++++++++-------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index cc5ece35..9bbe7657 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -52,7 +52,12 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } defer os.Remove(mmOutput.Name()) - args = append(args, ignoreIncludesArgs(includes)...) + includeTrie := NewTrie(WithReversePathSegmenter()) + + for _, inc := range includes { + includeTrie.Insert(inc) + args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc)) + } args = append(args, "-MM", "-MF", mmOutput.Name()) @@ -69,9 +74,8 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { panic(err) } - inters := make(map[string]struct{}) - includeMap := ParseMMOutout(outfile.Name(), mmOutput) var others []string + inters := RetrieveInterfaceFromMM(outfile.Name(), mmOutput, includeTrie) clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile @@ -81,23 +85,10 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { if filename == outfile.Name() { return } - refcnt := len(incins) - for _, inc := range incins { - incFileName := clang.GoString(inc.File().FileName()) - if incFileName == outfile.Name() { - refcnt-- - continue - } - if _, ok := includeMap[incFileName]; ok { - refcnt-- - } - } - - if refcnt == 0 { - inters[filename] = struct{}{} + if _, isInterface := inters[filename]; !isInterface { + others = append(others, filename) } - others = append(others, filename) }) info.Inters = slices.Collect(maps.Keys(inters)) @@ -108,9 +99,6 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } for _, filename := range others { - if _, isInterface := inters[filename]; isInterface { - continue - } if mix { info.Thirds = append(info.Thirds, filename) continue @@ -127,7 +115,6 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } sort.Strings(info.Inters) - sort.Strings(info.Impls) return info } @@ -154,30 +141,28 @@ func CommonParentDir(paths []string) string { return filepath.Dir(paths[0]) } -func ParseMMOutout(composedHeaderFileName string, outputFile *os.File) (includeMap map[string]struct{}) { +func RetrieveInterfaceFromMM( + composedHeaderFileName string, + mmOutput *os.File, + includeTrie *Trie, +) (interfaceMap map[string]struct{}) { fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") - includeMap = make(map[string]struct{}) - - content, _ := io.ReadAll(outputFile) + interfaceMap = make(map[string]struct{}) - fmt.Fprintln(os.Stderr, "aaaaa", string(content)) + content, _ := io.ReadAll(mmOutput) for _, line := range strings.Fields(string(content)) { // skip composed header file if strings.Contains(line, fileName) || line == `\` { continue } + headerFile := filepath.Clean(line) - includeMap[filepath.Clean(line)] = struct{}{} + if includeTrie.Contains(headerFile) { + interfaceMap[headerFile] = struct{}{} + } } return } - -func ignoreIncludesArgs(includes []string) (args []string) { - for _, inc := range includes { - args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc)) - } - return -} From 3776c37bd2d7a0d0d12088c2a1986e6ed4607ae0 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 16:36:22 +0800 Subject: [PATCH 27/36] fix: contains logic --- _xtool/internal/header/trie.go | 13 +++++++++---- _xtool/internal/header/trie_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go index 7469588e..af64ea6a 100644 --- a/_xtool/internal/header/trie.go +++ b/_xtool/internal/header/trie.go @@ -183,11 +183,16 @@ func (t *Trie) Contains(s string) bool { for segment := range t.segmenter(s) { child, ok := node.children[segment] + // if the current node is end, but there's something unmatched, we still consider it valid. + // for example, + // input: /c/b/a, tree: /c/b, valid + // input: /c/b/a, tree: /c/b/c, invalid + // input: /c/b, tree: /c/b/c, valid + if !ok && node.isLeaf { + return true + } if !ok { - if node == t.root { - node = nil - } - break + return false } node = child } diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index 0c60b43a..f528841e 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -83,6 +83,30 @@ func TestTrieContains(t *testing.T) { want: true, }, + { + name: "substring string case 4", + search: "/c/b", + inserted: []string{"/a/b", "/c/b/a"}, + want: true, + }, + { + name: "substring string case 5", + search: "/c/a", + inserted: []string{"/a/b", "/c/b/a"}, + want: false, + }, + { + name: "substring string case 6", + search: "/c/b/c", + inserted: []string{"/a/b", "/c/b/a"}, + want: false, + }, + { + name: "substring string case 7", + search: "/c/b", + inserted: []string{"/a/b", "/c/b/c/a"}, + want: true, + }, { name: "absolute path case 1", search: "a", @@ -356,7 +380,7 @@ func TestTrieReverse(t *testing.T) { { name: "multiple string case 4", - search: "/c/d", + search: "c/d", inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, want: true, }, From 690b3f8e6690e4a2d1f505761fbe1d313ef7b3cd Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 16:50:14 +0800 Subject: [PATCH 28/36] test: add more tests --- _xtool/internal/header/trie_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index f528841e..d995bdf4 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -432,6 +432,19 @@ func TestTrieReverse(t *testing.T) { }, want: false, }, + + { + name: "normal case 3", + search: "libxslt/imports.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/zlib/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxml2/imports.h", + + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: false, + }, } for _, tc := range testCases { From 8f81a2b52bd9c0f778760cdbf1e2f6073e535f5c Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 17:04:40 +0800 Subject: [PATCH 29/36] chore: rename Contains --- _xtool/internal/header/trie.go | 19 ++++++++----------- _xtool/internal/header/trie_test.go | 6 +++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go index af64ea6a..4d57c5c2 100644 --- a/_xtool/internal/header/trie.go +++ b/_xtool/internal/header/trie.go @@ -174,8 +174,8 @@ func (t *Trie) LongestPrefix(s string) string { return filepath.Join(prefix...) } -// Checks if the trie contains the given string as a prefix -func (t *Trie) Contains(s string) bool { +// IsSubsetOf checks the given s is the subset of trie tree +func (t *Trie) IsSubsetOf(s string) bool { if s == "" { return false } @@ -183,16 +183,13 @@ func (t *Trie) Contains(s string) bool { for segment := range t.segmenter(s) { child, ok := node.children[segment] - // if the current node is end, but there's something unmatched, we still consider it valid. - // for example, - // input: /c/b/a, tree: /c/b, valid - // input: /c/b/a, tree: /c/b/c, invalid - // input: /c/b, tree: /c/b/c, valid - if !ok && node.isLeaf { - return true - } if !ok { - return false + // if the current node is end, but there's something unmatched, we still consider it valid. + // for example, + // input: /c/b/a, tree: /c/b, valid + // input: /c/b/a, tree: /c/b/c, invalid + // input: /c/b, tree: /c/b/c, valid + return node.isLeaf } node = child } diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index d995bdf4..56bf249c 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -6,7 +6,7 @@ import ( "github.com/goplus/llcppg/_xtool/internal/header" ) -func TestTrieContains(t *testing.T) { +func TestTrieSubset(t *testing.T) { testCases := []struct { name string search string @@ -128,7 +128,7 @@ func TestTrieContains(t *testing.T) { for _, i := range tc.inserted { trie.Insert(i) } - if got := trie.Contains(tc.search); got != tc.want { + if got := trie.IsSubsetOf(tc.search); got != tc.want { t.Fatalf("unexpected result: want %v got %v", tc.want, got) } }) @@ -454,7 +454,7 @@ func TestTrieReverse(t *testing.T) { for _, i := range tc.inserted { trie.Insert(i) } - if got := trie.Contains(tc.search); got != tc.want { + if got := trie.IsSubsetOf(tc.search); got != tc.want { t.Fatalf("unexpected result: want %v got %v", tc.want, got) } }) From 1102c985b95e718029a7af0fc98c0df744fda5a6 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 17:06:08 +0800 Subject: [PATCH 30/36] test: remove duplicated test --- _xtool/internal/header/trie_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index 56bf249c..d816b00a 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -172,12 +172,6 @@ func TestTrieSearch(t *testing.T) { inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, want: false, }, - { - name: "Mixed path separators", - search: "/usr/local/bin/", - inserted: []string{"/usr/local/bin/"}, - want: true, - }, { name: "Non-existent path", search: "/non/existent/path", From a766abbd071722b18d2eef0a6750348465223817 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 17:15:00 +0800 Subject: [PATCH 31/36] feat: use trie to replace commonDir --- _xtool/internal/header/header.go | 21 +++++++++++++++++---- _xtool/internal/header/header_test.go | 6 +++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index 9bbe7657..3801b718 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -75,7 +75,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } var others []string - inters := RetrieveInterfaceFromMM(outfile.Name(), mmOutput, includeTrie) + inters, longestPrefix := RetrieveInterfaceFromMM(outfile.Name(), mmOutput, includeTrie) clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile @@ -93,7 +93,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { info.Inters = slices.Collect(maps.Keys(inters)) - absLongestPrefix, err := filepath.Abs(CommonParentDir(info.Inters)) + absLongestPrefix, err := filepath.Abs(longestPrefix) if err != nil { panic(err) } @@ -145,13 +145,16 @@ func RetrieveInterfaceFromMM( composedHeaderFileName string, mmOutput *os.File, includeTrie *Trie, -) (interfaceMap map[string]struct{}) { +) (interfaceMap map[string]struct{}, prefix string) { fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") interfaceMap = make(map[string]struct{}) content, _ := io.ReadAll(mmOutput) + mmTrie := NewTrie() + + var longestPrefix string for _, line := range strings.Fields(string(content)) { // skip composed header file if strings.Contains(line, fileName) || line == `\` { @@ -159,10 +162,20 @@ func RetrieveInterfaceFromMM( } headerFile := filepath.Clean(line) - if includeTrie.Contains(headerFile) { + if includeTrie.IsSubsetOf(headerFile) { + if longestPrefix == "" { + longestPrefix = headerFile + } else { + mmTrie.Insert(headerFile) + } + interfaceMap[headerFile] = struct{}{} } } + if longestPrefix != "" { + prefix = mmTrie.LongestPrefix(longestPrefix) + } + return } diff --git a/_xtool/internal/header/header_test.go b/_xtool/internal/header/header_test.go index d8cf0ed9..175d1a8e 100644 --- a/_xtool/internal/header/header_test.go +++ b/_xtool/internal/header/header_test.go @@ -153,17 +153,17 @@ func benchmarkFn(fn func()) time.Duration { return time.Since(now) } -func BenchmarkPkgHfileInfo(t *testing.B) { +func TestBenchmarkPkgHfileInfo(t *testing.T) { include := []string{"temp1.h", "temp2.h"} cflags := []string{"-I./testdata/hfile", "-I./testdata/thirdhfile"} t1 := benchmarkFn(func() { - for i := 0; i < t.N; i++ { + for i := 0; i < 100; i++ { pkgHfileInfo(include, cflags, false) } }) t2 := benchmarkFn(func() { - for i := 0; i < t.N; i++ { + for i := 0; i < 100; i++ { header.PkgHfileInfo(include, cflags, false) } }) From 1315fe9f458c536a8d3a0fdfda73f0ca4d098502 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 17:26:05 +0800 Subject: [PATCH 32/36] add test --- _xtool/internal/header/header.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index 3801b718..5e3dc084 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -98,6 +98,8 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { panic(err) } + fmt.Println(longestPrefix, CommonParentDir(info.Inters)) + for _, filename := range others { if mix { info.Thirds = append(info.Thirds, filename) From 7a11f7cffad71c27308d7cd2840e7ece94dcb035 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 17:31:13 +0800 Subject: [PATCH 33/36] add test --- _xtool/internal/header/header.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index 5e3dc084..09042220 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -98,7 +98,7 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { panic(err) } - fmt.Println(longestPrefix, CommonParentDir(info.Inters)) + fmt.Fprintln(os.Stderr, "tttttt", longestPrefix, CommonParentDir(info.Inters)) for _, filename := range others { if mix { From 378033970f038da7a46e9474d650fa2c54225ede Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 18:49:47 +0800 Subject: [PATCH 34/36] feat: use DFS to scan the longest common prefix --- _xtool/internal/header/trie.go | 28 ++++++++++-------- _xtool/internal/header/trie_test.go | 44 +++++++++-------------------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go index 4d57c5c2..b246c3ee 100644 --- a/_xtool/internal/header/trie.go +++ b/_xtool/internal/header/trie.go @@ -153,25 +153,29 @@ func (t *Trie) searchPrefix(s string) *TrieNode { // Returns the longest prefix that exists in the trie // // Implement Source: https://leetcode.com/problems/longest-common-prefix/solutions/127449/longest-common-prefix -func (t *Trie) LongestPrefix(s string) string { +func (t *Trie) LongestPrefix() string { var prefix []string - node := t.root - - for segment := range t.segmenter(s) { - child := node.children[segment] + dfs(&prefix, "", t.root, nil) - isLongestPrefix := child != nil && node.linkCount == 1 && !node.isLeaf + return filepath.Join(prefix...) +} - if !isLongestPrefix { - break - } +func dfs(prefix *[]string, currentPrefix string, node, parent *TrieNode) { + if node == nil { + return + } + if parent != nil && (parent.linkCount != 1 || parent.isLeaf) { + return + } - prefix = append(prefix, segment) - node = child + if currentPrefix != "" { + *prefix = append(*prefix, currentPrefix) } - return filepath.Join(prefix...) + for current, child := range node.children { + dfs(prefix, current, child, node) + } } // IsSubsetOf checks the given s is the subset of trie tree diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index d816b00a..a01fb314 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -221,67 +221,51 @@ func TestTrieLongestPrefix(t *testing.T) { tests := []struct { name string inserted []string - input string want string }{ { name: "Empty trie", inserted: []string{}, - input: "/usr/local/bin", want: "", }, { name: "Single directory exact match", inserted: []string{"/usr/local/bin/"}, - input: "/usr/local/bin", want: "/usr/local/bin", }, { name: "Single directory partial match", - inserted: []string{"/usr/local/bin/"}, - input: "/usr/local/bin/python", + inserted: []string{"/usr/local/bin/", "/usr/local/bin/python"}, want: "/usr/local/bin", }, { name: "Multiple directories with common prefix", - inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, - input: "/usr/local/bin/python", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/", "/usr/local/bin/python"}, want: "/usr", }, { name: "No common prefix", - inserted: []string{"/home/user/", "/var/log/", "/tmp/"}, - input: "/etc/passwd", + inserted: []string{"/home/user/", "/var/log/", "/tmp/", "/etc/passwd"}, want: "", }, { name: "Reverse path match", - inserted: []string{"bin", "lib", "include"}, - input: "include/lib/bin", + inserted: []string{"bin", "lib", "include", "include/lib/bin"}, want: "", }, { name: "Longer input than stored", - inserted: []string{"/short/"}, - input: "/shorter/path", - want: "", - }, - { - name: "Empty input", - inserted: []string{"/test/"}, - input: "", + inserted: []string{"/short/", "/shorter/path"}, want: "", }, { name: "No match", - inserted: []string{"/apple/", "/banana/"}, - input: "/cherry/", + inserted: []string{"/apple/", "/banana/", "/cherry/"}, want: "", }, { name: "Partial reverse match", - inserted: []string{"bin", "lib", "include"}, - input: "lib/bin", + inserted: []string{"bin", "lib", "include", "lib/bin"}, want: "", }, { @@ -289,20 +273,18 @@ func TestTrieLongestPrefix(t *testing.T) { inserted: []string{ "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h", + "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", }, - input: "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", - want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + want: "/opt/homebrew/Cellar/cjson/1.7.18/include", }, { name: "absolute path case 1", - inserted: []string{"/usr", "usr", "/usr/include"}, - input: "/usr", + inserted: []string{"/usr", "usr", "/usr/include", "/usr"}, want: "", }, { name: "absolute path case 2", - inserted: []string{"usr/share", "/usr", "usr/include"}, - input: "usr/include/share", + inserted: []string{"usr/share", "/usr", "usr/include", "usr/include/share"}, want: "", }, } @@ -313,9 +295,9 @@ func TestTrieLongestPrefix(t *testing.T) { for _, word := range tt.inserted { trie.Insert(word) } - result := trie.LongestPrefix(tt.input) + result := trie.LongestPrefix() if result != tt.want { - t.Errorf("LongestPrefix(%q) = %q, want %q", tt.input, result, tt.want) + t.Errorf("LongestPrefix(%q) = %q, want %q", tt.inserted, result, tt.want) } }) } From c0263ed9726ddffc1da0ea0d88665c2b23a72210 Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 18:53:08 +0800 Subject: [PATCH 35/36] chore: rename IsSubset --- _xtool/internal/header/trie.go | 4 ++-- _xtool/internal/header/trie_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go index b246c3ee..bfdd00d7 100644 --- a/_xtool/internal/header/trie.go +++ b/_xtool/internal/header/trie.go @@ -178,8 +178,8 @@ func dfs(prefix *[]string, currentPrefix string, node, parent *TrieNode) { } } -// IsSubsetOf checks the given s is the subset of trie tree -func (t *Trie) IsSubsetOf(s string) bool { +// IsOnSameBranch checks the given s is the subset of trie tree +func (t *Trie) IsOnSameBranch(s string) bool { if s == "" { return false } diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go index a01fb314..d770ab3b 100644 --- a/_xtool/internal/header/trie_test.go +++ b/_xtool/internal/header/trie_test.go @@ -128,7 +128,7 @@ func TestTrieSubset(t *testing.T) { for _, i := range tc.inserted { trie.Insert(i) } - if got := trie.IsSubsetOf(tc.search); got != tc.want { + if got := trie.IsOnSameBranch(tc.search); got != tc.want { t.Fatalf("unexpected result: want %v got %v", tc.want, got) } }) @@ -430,7 +430,7 @@ func TestTrieReverse(t *testing.T) { for _, i := range tc.inserted { trie.Insert(i) } - if got := trie.IsSubsetOf(tc.search); got != tc.want { + if got := trie.IsOnSameBranch(tc.search); got != tc.want { t.Fatalf("unexpected result: want %v got %v", tc.want, got) } }) From 8c2507262083c6ab20d3b251ea79d2245677107d Mon Sep 17 00:00:00 2001 From: Haolan Date: Fri, 6 Jun 2025 19:07:00 +0800 Subject: [PATCH 36/36] fix: trie name --- _xtool/internal/header/header.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index 09042220..45021f53 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -156,7 +156,6 @@ func RetrieveInterfaceFromMM( mmTrie := NewTrie() - var longestPrefix string for _, line := range strings.Fields(string(content)) { // skip composed header file if strings.Contains(line, fileName) || line == `\` { @@ -164,20 +163,12 @@ func RetrieveInterfaceFromMM( } headerFile := filepath.Clean(line) - if includeTrie.IsSubsetOf(headerFile) { - if longestPrefix == "" { - longestPrefix = headerFile - } else { - mmTrie.Insert(headerFile) - } + if includeTrie.IsOnSameBranch(headerFile) { + mmTrie.Insert(headerFile) interfaceMap[headerFile] = struct{}{} } } - - if longestPrefix != "" { - prefix = mmTrie.LongestPrefix(longestPrefix) - } - + prefix = mmTrie.LongestPrefix() return }