1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5use std::time::Instant;
6
7use anyhow::{Context, Result};
8use tempfile::{Builder, TempDir};
9
10use super::{
11 ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, cache_store,
12 execution_timeout, hash_source, perf_record, run_version_command, try_cached_execution,
13 wait_with_timeout,
14};
15
16pub struct GoEngine {
17 executable: Option<PathBuf>,
18}
19
20impl Default for GoEngine {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl GoEngine {
27 pub fn new() -> Self {
28 Self {
29 executable: resolve_go_binary(),
30 }
31 }
32
33 fn ensure_executable(&self) -> Result<&Path> {
34 self.executable.as_deref().ok_or_else(|| {
35 anyhow::anyhow!(
36 "Go support requires the `go` executable. Install it from https://go.dev/dl/ and ensure it is on your PATH."
37 )
38 })
39 }
40
41 fn write_temp_source(&self, code: &str) -> Result<(tempfile::TempDir, PathBuf)> {
42 let dir = Builder::new()
43 .prefix("run-go")
44 .tempdir()
45 .context("failed to create temporary directory for go source")?;
46 let path = dir.path().join("main.go");
47 let mut contents = code.to_string();
48 if !contents.ends_with('\n') {
49 contents.push('\n');
50 }
51 std::fs::write(&path, contents).with_context(|| {
52 format!("failed to write temporary Go source to {}", path.display())
53 })?;
54 Ok((dir, path))
55 }
56
57 fn execute_with_path(
58 &self,
59 binary: &Path,
60 source: &Path,
61 args: &[String],
62 ) -> Result<std::process::Output> {
63 let mut cmd = Command::new(binary);
64 cmd.arg("run")
65 .stdout(Stdio::piped())
66 .stderr(Stdio::piped())
67 .env("GO111MODULE", "off");
68 cmd.stdin(Stdio::inherit());
69
70 if let Some(parent) = source.parent() {
71 cmd.current_dir(parent);
72 if let Some(file_name) = source.file_name() {
73 cmd.arg(file_name);
74 } else {
75 cmd.arg(source);
76 }
77 cmd.args(args);
78 } else {
79 cmd.arg(source).args(args);
80 }
81 let child = cmd.spawn().with_context(|| {
82 format!(
83 "failed to invoke {} to run {}",
84 binary.display(),
85 source.display()
86 )
87 })?;
88 wait_with_timeout(child, execution_timeout())
89 }
90}
91
92impl LanguageEngine for GoEngine {
93 fn id(&self) -> &'static str {
94 "go"
95 }
96
97 fn display_name(&self) -> &'static str {
98 "Go"
99 }
100
101 fn aliases(&self) -> &[&'static str] {
102 &["golang"]
103 }
104
105 fn supports_sessions(&self) -> bool {
106 true
107 }
108
109 fn validate(&self) -> Result<()> {
110 let binary = self.ensure_executable()?;
111 let mut cmd = Command::new(binary);
112 cmd.arg("version")
113 .stdout(Stdio::null())
114 .stderr(Stdio::null());
115 cmd.status()
116 .with_context(|| format!("failed to invoke {}", binary.display()))?
117 .success()
118 .then_some(())
119 .ok_or_else(|| anyhow::anyhow!("{} is not executable", binary.display()))
120 }
121
122 fn toolchain_version(&self) -> Result<Option<String>> {
123 let binary = self.ensure_executable()?;
124 let mut cmd = Command::new(binary);
125 cmd.arg("version");
126 let context = format!("{}", binary.display());
127 run_version_command(cmd, &context)
128 }
129
130 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
131 let args = payload.args();
133 if let ExecutionPayload::File { path, .. } = payload {
134 let start = Instant::now();
135 let source_text = fs::read_to_string(path).unwrap_or_default();
136 let src_hash = hash_source(&source_text);
137 if let Some(output) = try_cached_execution("go-file", src_hash) {
138 perf_record("go", "file.cache_hit");
139 return Ok(ExecutionOutcome {
140 language: self.id().to_string(),
141 exit_code: output.status.code(),
142 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
143 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
144 duration: start.elapsed(),
145 });
146 }
147 perf_record("go", "file.cache_miss");
148
149 let binary = self.ensure_executable()?;
150 let temp_dir = Builder::new()
151 .prefix("run-go-file")
152 .tempdir()
153 .context("failed to create temporary directory for go file build")?;
154 let bin_path = temp_dir.path().join("run_go_file_binary");
155 let mut build_cmd = Command::new(binary);
156 perf_record("go", "file.build");
157 build_cmd
158 .arg("build")
159 .arg("-o")
160 .arg(&bin_path)
161 .arg(path)
162 .env("GO111MODULE", "off")
163 .stdout(Stdio::piped())
164 .stderr(Stdio::piped());
165 let build_output = build_cmd.output().with_context(|| {
166 format!("failed to invoke {} to build Go source", binary.display())
167 })?;
168 if !build_output.status.success() {
169 perf_record("go", "file.build_fail");
170 return Ok(ExecutionOutcome {
171 language: self.id().to_string(),
172 exit_code: build_output.status.code(),
173 stdout: String::from_utf8_lossy(&build_output.stdout).into_owned(),
174 stderr: String::from_utf8_lossy(&build_output.stderr).into_owned(),
175 duration: start.elapsed(),
176 });
177 }
178
179 let cached_bin =
180 cache_store("go-file", src_hash, &bin_path).unwrap_or(bin_path.clone());
181 let mut run_cmd = Command::new(&cached_bin);
182 run_cmd
183 .args(args)
184 .stdout(Stdio::piped())
185 .stderr(Stdio::piped())
186 .stdin(Stdio::inherit());
187 let child = run_cmd.spawn().with_context(|| {
188 format!(
189 "failed to execute compiled Go binary {}",
190 cached_bin.display()
191 )
192 })?;
193 let output = wait_with_timeout(child, execution_timeout())?;
194 return Ok(ExecutionOutcome {
195 language: self.id().to_string(),
196 exit_code: output.status.code(),
197 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
198 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
199 duration: start.elapsed(),
200 });
201 }
202
203 if let Some(code) = match payload {
204 ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
205 Some(code.as_str())
206 }
207 _ => None,
208 } {
209 let src_hash = hash_source(code);
210 if let Some(output) = try_cached_execution("go", src_hash) {
211 perf_record("go", "inline.cache_hit");
212 let start = Instant::now();
213 return Ok(ExecutionOutcome {
214 language: self.id().to_string(),
215 exit_code: output.status.code(),
216 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
217 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
218 duration: start.elapsed(),
219 });
220 }
221 perf_record("go", "inline.cache_miss");
222 }
223
224 let binary = self.ensure_executable()?;
225 let start = Instant::now();
226
227 let (temp_dir, source_path, cache_key) = match payload {
228 ExecutionPayload::Inline { code, .. } => {
229 let h = hash_source(code);
230 let (dir, path) = self.write_temp_source(code)?;
231 (Some(dir), path, Some(h))
232 }
233 ExecutionPayload::Stdin { code, .. } => {
234 let h = hash_source(code);
235 let (dir, path) = self.write_temp_source(code)?;
236 (Some(dir), path, Some(h))
237 }
238 ExecutionPayload::File { path, .. } => (None, path.clone(), None),
239 };
240
241 if let Some(h) = cache_key {
243 let dir = source_path.parent().unwrap_or(std::path::Path::new("."));
244 let bin_path = dir.join("run_go_binary");
245 let mut build_cmd = Command::new(binary);
246 build_cmd
247 .arg("build")
248 .arg("-o")
249 .arg(&bin_path)
250 .env("GO111MODULE", "off")
251 .stdout(Stdio::piped())
252 .stderr(Stdio::piped());
253 if let Some(file_name) = source_path.file_name() {
254 build_cmd.current_dir(dir).arg(file_name);
255 } else {
256 build_cmd.arg(&source_path);
257 }
258
259 let build_output = build_cmd.output().with_context(|| {
260 format!("failed to invoke {} to build Go source", binary.display())
261 })?;
262
263 if !build_output.status.success() {
264 return Ok(ExecutionOutcome {
265 language: self.id().to_string(),
266 exit_code: build_output.status.code(),
267 stdout: String::from_utf8_lossy(&build_output.stdout).into_owned(),
268 stderr: String::from_utf8_lossy(&build_output.stderr).into_owned(),
269 duration: start.elapsed(),
270 });
271 }
272
273 cache_store("go", h, &bin_path);
274
275 let mut run_cmd = Command::new(&bin_path);
276 run_cmd
277 .args(args)
278 .stdout(Stdio::piped())
279 .stderr(Stdio::piped())
280 .stdin(Stdio::inherit());
281 let child = run_cmd.spawn().with_context(|| {
282 format!(
283 "failed to execute compiled Go binary {}",
284 bin_path.display()
285 )
286 })?;
287 let output = wait_with_timeout(child, execution_timeout())?;
288
289 drop(temp_dir);
290 return Ok(ExecutionOutcome {
291 language: self.id().to_string(),
292 exit_code: output.status.code(),
293 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
294 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
295 duration: start.elapsed(),
296 });
297 }
298
299 let output = self.execute_with_path(binary, &source_path, args)?;
300 drop(temp_dir);
301
302 Ok(ExecutionOutcome {
303 language: self.id().to_string(),
304 exit_code: output.status.code(),
305 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
306 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
307 duration: start.elapsed(),
308 })
309 }
310
311 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
312 let binary = self.ensure_executable()?.to_path_buf();
313 let session = GoSession::new(binary)?;
314 Ok(Box::new(session))
315 }
316}
317
318fn resolve_go_binary() -> Option<PathBuf> {
319 which::which("go").ok()
320}
321
322fn import_is_used_in_code(import: &str, code: &str) -> bool {
323 let import_trimmed = import.trim().trim_matches('"');
324 let package_name = import_trimmed.rsplit('/').next().unwrap_or(import_trimmed);
325 let pattern = format!("{}.", package_name);
326 code.contains(&pattern)
327}
328
329const SESSION_MAIN_FILE: &str = "main.go";
330
331struct GoSession {
332 go_binary: PathBuf,
333 workspace: TempDir,
334 imports: BTreeSet<String>,
335 items: Vec<String>,
336 statements: Vec<String>,
337 last_stdout: String,
338 last_stderr: String,
339}
340
341enum GoSnippetKind {
342 Import(Option<String>),
343 Item,
344 Statement,
345}
346
347impl GoSession {
348 fn new(go_binary: PathBuf) -> Result<Self> {
349 let workspace = TempDir::new().context("failed to create Go session workspace")?;
350 let mut imports = BTreeSet::new();
351 imports.insert("\"fmt\"".to_string());
352 let session = Self {
353 go_binary,
354 workspace,
355 imports,
356 items: Vec::new(),
357 statements: Vec::new(),
358 last_stdout: String::new(),
359 last_stderr: String::new(),
360 };
361 session.persist_source()?;
362 Ok(session)
363 }
364
365 fn language_id(&self) -> &str {
366 "go"
367 }
368
369 fn source_path(&self) -> PathBuf {
370 self.workspace.path().join(SESSION_MAIN_FILE)
371 }
372
373 fn persist_source(&self) -> Result<()> {
374 let source = self.render_source();
375 fs::write(self.source_path(), source)
376 .with_context(|| "failed to write Go session source".to_string())
377 }
378
379 fn render_source(&self) -> String {
380 let mut source = String::from("package main\n\n");
381
382 if !self.imports.is_empty() {
383 source.push_str("import (\n");
384 for import in &self.imports {
385 source.push('\t');
386 source.push_str(import);
387 source.push('\n');
388 }
389 source.push_str(")\n\n");
390 }
391
392 source.push_str(concat!(
393 "func __print(value interface{}) {\n",
394 "\tif s, ok := value.(string); ok {\n",
395 "\t\tfmt.Println(s)\n",
396 "\t\treturn\n",
397 "\t}\n",
398 "\tfmt.Printf(\"%#v\\n\", value)\n",
399 "}\n\n",
400 ));
401
402 for item in &self.items {
403 source.push_str(item);
404 if !item.ends_with('\n') {
405 source.push('\n');
406 }
407 source.push('\n');
408 }
409
410 source.push_str("func main() {\n");
411 if self.statements.is_empty() {
412 source.push_str("\t// session body\n");
413 } else {
414 for snippet in &self.statements {
415 for line in snippet.lines() {
416 source.push('\t');
417 source.push_str(line);
418 source.push('\n');
419 }
420 }
421 }
422 source.push_str("}\n");
423
424 source
425 }
426
427 fn run_program(&self) -> Result<std::process::Output> {
428 let mut cmd = Command::new(&self.go_binary);
429 cmd.arg("run")
430 .arg(SESSION_MAIN_FILE)
431 .env("GO111MODULE", "off")
432 .stdout(Stdio::piped())
433 .stderr(Stdio::piped())
434 .current_dir(self.workspace.path());
435 cmd.output().with_context(|| {
436 format!(
437 "failed to execute {} for Go session",
438 self.go_binary.display()
439 )
440 })
441 }
442
443 fn run_standalone_program(&self, code: &str) -> Result<ExecutionOutcome> {
444 let start = Instant::now();
445 let standalone_path = self.workspace.path().join("standalone.go");
446
447 let source = if has_package_declaration(code) {
448 let mut snippet = code.to_string();
449 if !snippet.ends_with('\n') {
450 snippet.push('\n');
451 }
452 snippet
453 } else {
454 let mut source = String::from("package main\n\n");
455
456 let used_imports: Vec<_> = self
457 .imports
458 .iter()
459 .filter(|import| import_is_used_in_code(import, code))
460 .cloned()
461 .collect();
462
463 if !used_imports.is_empty() {
464 source.push_str("import (\n");
465 for import in &used_imports {
466 source.push('\t');
467 source.push_str(import);
468 source.push('\n');
469 }
470 source.push_str(")\n\n");
471 }
472
473 source.push_str(code);
474 if !code.ends_with('\n') {
475 source.push('\n');
476 }
477 source
478 };
479
480 fs::write(&standalone_path, source)
481 .with_context(|| "failed to write Go standalone source".to_string())?;
482
483 let mut cmd = Command::new(&self.go_binary);
484 cmd.arg("run")
485 .arg("standalone.go")
486 .env("GO111MODULE", "off")
487 .stdout(Stdio::piped())
488 .stderr(Stdio::piped())
489 .current_dir(self.workspace.path());
490
491 let output = cmd.output().with_context(|| {
492 format!(
493 "failed to execute {} for Go standalone program",
494 self.go_binary.display()
495 )
496 })?;
497
498 let outcome = ExecutionOutcome {
499 language: self.language_id().to_string(),
500 exit_code: output.status.code(),
501 stdout: Self::normalize_output(&output.stdout),
502 stderr: Self::normalize_output(&output.stderr),
503 duration: start.elapsed(),
504 };
505
506 let _ = fs::remove_file(&standalone_path);
507
508 Ok(outcome)
509 }
510
511 fn add_import(&mut self, spec: &str) -> GoSnippetKind {
512 let added = self.imports.insert(spec.to_string());
513 if added {
514 GoSnippetKind::Import(Some(spec.to_string()))
515 } else {
516 GoSnippetKind::Import(None)
517 }
518 }
519
520 fn add_item(&mut self, code: &str) -> GoSnippetKind {
521 let mut snippet = code.to_string();
522 if !snippet.ends_with('\n') {
523 snippet.push('\n');
524 }
525 self.items.push(snippet);
526 GoSnippetKind::Item
527 }
528
529 fn add_statement(&mut self, code: &str) -> GoSnippetKind {
530 let snippet = sanitize_statement(code);
531 self.statements.push(snippet);
532 GoSnippetKind::Statement
533 }
534
535 fn add_expression(&mut self, code: &str) -> GoSnippetKind {
536 let wrapped = wrap_expression(code);
537 self.statements.push(wrapped);
538 GoSnippetKind::Statement
539 }
540
541 fn rollback(&mut self, kind: GoSnippetKind) -> Result<()> {
542 match kind {
543 GoSnippetKind::Import(Some(spec)) => {
544 self.imports.remove(&spec);
545 }
546 GoSnippetKind::Import(None) => {}
547 GoSnippetKind::Item => {
548 self.items.pop();
549 }
550 GoSnippetKind::Statement => {
551 self.statements.pop();
552 }
553 }
554 self.persist_source()
555 }
556
557 fn normalize_output(bytes: &[u8]) -> String {
558 String::from_utf8_lossy(bytes)
559 .replace("\r\n", "\n")
560 .replace('\r', "")
561 }
562
563 fn diff_outputs(previous: &str, current: &str) -> String {
564 if let Some(suffix) = current.strip_prefix(previous) {
565 suffix.to_string()
566 } else {
567 current.to_string()
568 }
569 }
570
571 fn run_insertion(&mut self, kind: GoSnippetKind) -> Result<(ExecutionOutcome, bool)> {
572 match kind {
573 GoSnippetKind::Import(None) => Ok((
574 ExecutionOutcome {
575 language: self.language_id().to_string(),
576 exit_code: None,
577 stdout: String::new(),
578 stderr: String::new(),
579 duration: Default::default(),
580 },
581 true,
582 )),
583 other_kind => {
584 self.persist_source()?;
585 let start = Instant::now();
586 let output = self.run_program()?;
587
588 let stdout_full = Self::normalize_output(&output.stdout);
589 let stderr_full = Self::normalize_output(&output.stderr);
590
591 let stdout = Self::diff_outputs(&self.last_stdout, &stdout_full);
592 let stderr = Self::diff_outputs(&self.last_stderr, &stderr_full);
593 let duration = start.elapsed();
594
595 if output.status.success() {
596 self.last_stdout = stdout_full;
597 self.last_stderr = stderr_full;
598 let outcome = ExecutionOutcome {
599 language: self.language_id().to_string(),
600 exit_code: output.status.code(),
601 stdout,
602 stderr,
603 duration,
604 };
605 return Ok((outcome, true));
606 }
607
608 if matches!(&other_kind, GoSnippetKind::Import(Some(_)))
609 && stderr_full.contains("imported and not used")
610 {
611 return Ok((
612 ExecutionOutcome {
613 language: self.language_id().to_string(),
614 exit_code: None,
615 stdout: String::new(),
616 stderr: String::new(),
617 duration,
618 },
619 true,
620 ));
621 }
622
623 self.rollback(other_kind)?;
624 let outcome = ExecutionOutcome {
625 language: self.language_id().to_string(),
626 exit_code: output.status.code(),
627 stdout,
628 stderr,
629 duration,
630 };
631 Ok((outcome, false))
632 }
633 }
634 }
635
636 fn run_import(&mut self, spec: &str) -> Result<(ExecutionOutcome, bool)> {
637 let kind = self.add_import(spec);
638 self.run_insertion(kind)
639 }
640
641 fn run_item(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
642 let kind = self.add_item(code);
643 self.run_insertion(kind)
644 }
645
646 fn run_statement(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
647 let kind = self.add_statement(code);
648 self.run_insertion(kind)
649 }
650
651 fn run_expression(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
652 let kind = self.add_expression(code);
653 self.run_insertion(kind)
654 }
655}
656
657impl LanguageSession for GoSession {
658 fn language_id(&self) -> &str {
659 GoSession::language_id(self)
660 }
661
662 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
663 let trimmed = code.trim();
664 if trimmed.is_empty() {
665 return Ok(ExecutionOutcome {
666 language: self.language_id().to_string(),
667 exit_code: None,
668 stdout: String::new(),
669 stderr: String::new(),
670 duration: Instant::now().elapsed(),
671 });
672 }
673
674 if trimmed.starts_with("package ") && !trimmed.contains('\n') {
675 return Ok(ExecutionOutcome {
676 language: self.language_id().to_string(),
677 exit_code: None,
678 stdout: String::new(),
679 stderr: String::new(),
680 duration: Instant::now().elapsed(),
681 });
682 }
683
684 if contains_main_definition(trimmed) {
685 let outcome = self.run_standalone_program(code)?;
686 return Ok(outcome);
687 }
688
689 if let Some(import) = parse_import_spec(trimmed) {
690 let (outcome, _) = self.run_import(&import)?;
691 return Ok(outcome);
692 }
693
694 if is_item_snippet(trimmed) {
695 let (outcome, _) = self.run_item(code)?;
696 return Ok(outcome);
697 }
698
699 if should_treat_as_expression(trimmed) {
700 let (outcome, success) = self.run_expression(trimmed)?;
701 if success {
702 return Ok(outcome);
703 }
704 }
705
706 let (outcome, _) = self.run_statement(code)?;
707 Ok(outcome)
708 }
709
710 fn shutdown(&mut self) -> Result<()> {
711 Ok(())
712 }
713}
714
715fn parse_import_spec(code: &str) -> Option<String> {
716 let trimmed = code.trim_start();
717 if !trimmed.starts_with("import ") {
718 return None;
719 }
720 let rest = trimmed.trim_start_matches("import").trim();
721 if rest.is_empty() || rest.starts_with('(') {
722 return None;
723 }
724 Some(rest.to_string())
725}
726
727fn is_item_snippet(code: &str) -> bool {
728 let trimmed = code.trim_start();
729 if trimmed.is_empty() {
730 return false;
731 }
732 const KEYWORDS: [&str; 6] = ["type", "const", "var", "func", "package", "import"];
733 KEYWORDS.iter().any(|kw| {
734 trimmed.starts_with(kw)
735 && trimmed
736 .chars()
737 .nth(kw.len())
738 .map(|ch| ch.is_whitespace() || ch == '(')
739 .unwrap_or(true)
740 })
741}
742
743fn should_treat_as_expression(code: &str) -> bool {
744 let trimmed = code.trim();
745 if trimmed.is_empty() {
746 return false;
747 }
748 if trimmed.contains('\n') {
749 return false;
750 }
751 if trimmed.ends_with(';') {
752 return false;
753 }
754 if trimmed.contains(":=") {
755 return false;
756 }
757 if trimmed.contains('=') && !trimmed.contains("==") {
758 return false;
759 }
760 const RESERVED: [&str; 8] = [
761 "if ", "for ", "switch ", "select ", "return ", "go ", "defer ", "var ",
762 ];
763 if RESERVED.iter().any(|kw| trimmed.starts_with(kw)) {
764 return false;
765 }
766 true
767}
768
769fn wrap_expression(code: &str) -> String {
770 format!("__print({});\n", code)
771}
772
773fn sanitize_statement(code: &str) -> String {
774 let mut snippet = code.to_string();
775 if !snippet.ends_with('\n') {
776 snippet.push('\n');
777 }
778
779 let trimmed = code.trim();
780 if trimmed.is_empty() || trimmed.contains('\n') {
781 return snippet;
782 }
783
784 let mut identifiers: Vec<String> = Vec::new();
785
786 if let Some(idx) = trimmed.find(" :=") {
787 let lhs = &trimmed[..idx];
788 identifiers = lhs
789 .split(',')
790 .map(|part| part.trim())
791 .filter(|name| !name.is_empty() && *name != "_")
792 .map(|name| name.to_string())
793 .collect();
794 } else if let Some(idx) = trimmed.find(':') {
795 if trimmed[idx..].starts_with(":=") {
796 let lhs = &trimmed[..idx];
797 identifiers = lhs
798 .split(',')
799 .map(|part| part.trim())
800 .filter(|name| !name.is_empty() && *name != "_")
801 .map(|name| name.to_string())
802 .collect();
803 }
804 } else if let Some(stripped) = trimmed.strip_prefix("var ") {
805 let rest = stripped.trim();
806 if !rest.starts_with('(') {
807 let names_part = rest.split('=').next().unwrap_or(rest).trim();
808 identifiers = names_part
809 .split(',')
810 .filter_map(|segment| {
811 let token = segment.split_whitespace().next().unwrap_or("");
812 if token.is_empty() || token == "_" {
813 None
814 } else {
815 Some(token.to_string())
816 }
817 })
818 .collect();
819 }
820 } else if let Some(stripped) = trimmed.strip_prefix("const ") {
821 let rest = stripped.trim();
822 if !rest.starts_with('(') {
823 let names_part = rest.split('=').next().unwrap_or(rest).trim();
824 identifiers = names_part
825 .split(',')
826 .filter_map(|segment| {
827 let token = segment.split_whitespace().next().unwrap_or("");
828 if token.is_empty() || token == "_" {
829 None
830 } else {
831 Some(token.to_string())
832 }
833 })
834 .collect();
835 }
836 }
837
838 if identifiers.is_empty() {
839 return snippet;
840 }
841
842 for name in identifiers {
843 snippet.push_str("_ = ");
844 snippet.push_str(&name);
845 snippet.push('\n');
846 }
847
848 snippet
849}
850
851fn has_package_declaration(code: &str) -> bool {
852 code.lines()
853 .any(|line| line.trim_start().starts_with("package "))
854}
855
856fn contains_main_definition(code: &str) -> bool {
857 let bytes = code.as_bytes();
858 let len = bytes.len();
859 let mut i = 0;
860 let mut in_line_comment = false;
861 let mut in_block_comment = false;
862 let mut in_string = false;
863 let mut string_delim = b'"';
864 let mut in_char = false;
865
866 while i < len {
867 let b = bytes[i];
868
869 if in_line_comment {
870 if b == b'\n' {
871 in_line_comment = false;
872 }
873 i += 1;
874 continue;
875 }
876
877 if in_block_comment {
878 if b == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
879 in_block_comment = false;
880 i += 2;
881 continue;
882 }
883 i += 1;
884 continue;
885 }
886
887 if in_string {
888 if b == b'\\' {
889 i = (i + 2).min(len);
890 continue;
891 }
892 if b == string_delim {
893 in_string = false;
894 }
895 i += 1;
896 continue;
897 }
898
899 if in_char {
900 if b == b'\\' {
901 i = (i + 2).min(len);
902 continue;
903 }
904 if b == b'\'' {
905 in_char = false;
906 }
907 i += 1;
908 continue;
909 }
910
911 match b {
912 b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
913 in_line_comment = true;
914 i += 2;
915 continue;
916 }
917 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
918 in_block_comment = true;
919 i += 2;
920 continue;
921 }
922 b'"' | b'`' => {
923 in_string = true;
924 string_delim = b;
925 i += 1;
926 continue;
927 }
928 b'\'' => {
929 in_char = true;
930 i += 1;
931 continue;
932 }
933 b'f' if i + 4 <= len && &bytes[i..i + 4] == b"func" => {
934 if i > 0 {
935 let prev = bytes[i - 1];
936 if prev.is_ascii_alphanumeric() || prev == b'_' {
937 i += 1;
938 continue;
939 }
940 }
941
942 let mut j = i + 4;
943 while j < len && bytes[j].is_ascii_whitespace() {
944 j += 1;
945 }
946
947 if j + 4 > len || &bytes[j..j + 4] != b"main" {
948 i += 1;
949 continue;
950 }
951
952 let after = j + 4;
953 if after < len {
954 let ch = bytes[after];
955 if ch.is_ascii_alphanumeric() || ch == b'_' {
956 i += 1;
957 continue;
958 }
959 }
960
961 let mut k = after;
962 while k < len && bytes[k].is_ascii_whitespace() {
963 k += 1;
964 }
965 if k < len && bytes[k] == b'(' {
966 return true;
967 }
968 }
969 _ => {}
970 }
971
972 i += 1;
973 }
974
975 false
976}