44package codeeditor
55
66import (
7+ "bytes"
8+ "os"
79 "path/filepath"
10+ "slices"
811 "strings"
912
1013 "github.com/google/syzkaller/pkg/aflow"
14+ "github.com/google/syzkaller/pkg/codesearch"
1115 "github.com/google/syzkaller/pkg/osutil"
1216)
1317
1418var Tool = aflow .NewFuncTool ("codeeditor" , codeeditor , `
15- The tool does one code edit to form the final patch.
19+ The tool does one source code edit to form the final patch by replacing full lines
20+ with new provided lines. If new code is empty, current lines will be deleted.
21+ Provide full lines of code including new line characters.
1622The tool should be called mutiple times to do all required changes one-by-one,
1723but avoid changing the same lines multiple times.
1824Note: You will not see your edits via the codesearch tool.
@@ -24,27 +30,91 @@ type state struct {
2430}
2531
2632type args struct {
27- SourceFile string `jsonschema:"Full source file path."`
28- CurrentCode string `jsonschema:"The current code to replace verbatim with new lines, but without line numbers ."`
29- NewCode string `jsonschema:"New code to replace the current code snippet ."`
33+ SourceFile string `jsonschema:"Full source file path to edit ."`
34+ CurrentCode string `jsonschema:"The current code lines to be replaced ."`
35+ NewCode string `jsonschema:"New code lines to replace the current code lines ."`
3036}
3137
3238func codeeditor (ctx * aflow.Context , state state , args args ) (struct {}, error ) {
3339 if strings .Contains (filepath .Clean (args .SourceFile ), ".." ) {
3440 return struct {}{}, aflow .BadCallError ("SourceFile %q is outside of the source tree" , args .SourceFile )
3541 }
3642 file := filepath .Join (state .KernelScratchSrc , args .SourceFile )
37- if ! osutil .IsExist (file ) {
43+ // Filter out not source files too (e.g. .git, etc),
44+ // LLM have not seen them and should not be messing with them.
45+ if ! osutil .IsExist (file ) || ! codesearch .IsSourceFile (file ) {
3846 return struct {}{}, aflow .BadCallError ("SourceFile %q does not exist" , args .SourceFile )
3947 }
4048 if strings .TrimSpace (args .CurrentCode ) == "" {
4149 return struct {}{}, aflow .BadCallError ("CurrentCode snippet is empty" )
4250 }
43- // If SourceFile is incorrect, or CurrentCode is not matched, return aflow.BadCallError
44- // with an explanation. Say that it needs to increase context if CurrentCode is not matched.
45- // Try to do as fuzzy match for CurrentCode as possible (strip line numbers,
46- // ignore white-spaces, etc).
47- // Should we accept a reference line number, or function name to disambiguate in the case
48- // of multiple matches?
49- return struct {}{}, nil
51+ fileData , err := os .ReadFile (file )
52+ if err != nil {
53+ return struct {}{}, err
54+ }
55+ if len (fileData ) == 0 || fileData [len (fileData )- 1 ] != '\n' {
56+ // Generally shouldn't happen, but just in case.
57+ fileData = append (fileData , '\n' )
58+ }
59+ if args .CurrentCode [len (args .CurrentCode )- 1 ] != '\n' {
60+ args .CurrentCode += "\n "
61+ }
62+ if args .NewCode != "" && args .NewCode [len (args .NewCode )- 1 ] != '\n' {
63+ args .NewCode += "\n "
64+ }
65+ lines := slices .Collect (bytes .Lines (fileData ))
66+ src := slices .Collect (bytes .Lines ([]byte (args .CurrentCode )))
67+ dst := slices .Collect (bytes .Lines ([]byte (args .NewCode )))
68+ // First, try to match as is. If that fails, try a more permissive matching
69+ // that ignores whitespaces, empty lines, etc.
70+ newLines , matches := replace (lines , src , dst , false )
71+ if matches == 0 {
72+ newLines , matches = replace (lines , src , dst , true )
73+ }
74+ if matches == 0 {
75+ return struct {}{}, aflow .BadCallError ("CurrentCode snippet does not match anything in the source file," +
76+ " provide more precise CurrentCode snippet" )
77+ }
78+ if matches > 1 {
79+ return struct {}{}, aflow .BadCallError ("CurrentCode snippet matched %v places," +
80+ " increase context in CurrentCode to avoid ambiguity" , matches )
81+ }
82+ err = osutil .WriteFile (file , slices .Concat (newLines ... ))
83+ return struct {}{}, err
84+ }
85+
86+ func replace (lines , src , dst [][]byte , fuzzy bool ) (newLines [][]byte , matches int ) {
87+ for i := 0 ; i < len (lines ); i ++ {
88+ li , si := i , 0
89+ for li < len (lines ) && si < len (src ) {
90+ l , s := lines [li ], src [si ]
91+ if fuzzy {
92+ // Ignore whitespaces and empty lines.
93+ l , s = bytes .TrimSpace (l ), bytes .TrimSpace (s )
94+ // Potentially we can remove line numbers from s here if they are present,
95+ // or use them to disambiguate in the case of multiple matches.
96+ if len (s ) == 0 {
97+ si ++
98+ continue
99+ }
100+ if len (l ) == 0 {
101+ li ++
102+ continue
103+ }
104+ }
105+ if ! bytes .Equal (l , s ) {
106+ break
107+ }
108+ li ++
109+ si ++
110+ }
111+ if si != len (src ) {
112+ newLines = append (newLines , lines [i ])
113+ continue
114+ }
115+ matches ++
116+ newLines = append (newLines , dst ... )
117+ i = li - 1
118+ }
119+ return
50120}
0 commit comments