Skip to content

Instantly share code, notes, and snippets.

@podhmo
Created June 2, 2025 16:01
Show Gist options
  • Save podhmo/6a0a06843e70a0cf5489b838a3159cfb to your computer and use it in GitHub Desktop.
Save podhmo/6a0a06843e70a0cf5489b838a3159cfb to your computer and use it in GitHub Desktop.

対話履歴

ユーザー: 【重要なお願い】 あなたは、私と対話するアシスタントです。 もし、私の最後の発言に特定のトリガーワードが含まれていたら、その直後のあなたの応答は特別なモードとして扱われます。

  1. トリガーワード ##SCAFFOLD_GENERATE## が含まれていた場合: scaffold生成モードとなり、以下の【出力形式のルール(生成モード)】に従って、ファイル構造と各ファイルの内容を厳密に出力してください。

  2. トリガーワード ##SCAFFOLD_PLAN## が含まれていた場合: プランニングモードとなり、以下の【出力形式のルール(プランニングモード)】に従って、提案するファイル名とその必要理由(または簡単な説明)を箇条書きで出力してください。

トリガーワードが私の発言に含まれていない通常の対話では、これらの形式に縛られる必要はありません。自由に回答してください。

【出力形式のルール(生成モード)】

  1. 各ファイルは、ファイルパスとその内容のペアで構成してください。
  2. ファイルパスは、プロジェクトのルートディレクトリからの相対パスで記述してください。
  3. ファイルの内容は、4つのバッククォート(````)で囲まれたMarkdownのコードブロックとして記述してください。
    • コードブロックの開始行には、言語識別子(オプション)とコロン : に続けてファイルパスを記述してください。
    • 例: ````typescript:src/components/Button.tsx
  4. 生成するファイルが複数ある場合は、上記1〜3のセットを繰り返してください。
  5. ファイルの内容にMarkdownのコードブロックが含まれる場合(例:README.md内にサンプルコードを記述する場合)、そのネストしたコードブロックは通常の3つのバッククォート(```)を使用してください。4つのバッククォートは最も外側のファイル単位のブロックでのみ使用します。

【出力形式のルール(プランニングモード)】

  1. 提案する各ファイルについて、以下の情報を1行で記述してください。
    • ファイルパス - 簡単な説明や必要理由
  2. ファイルパスは、プロジェクトのルートディレクトリからの相対パスで記述してください。
  3. 箇条書き(例: - * で始まる行)で列挙してください。

【出力例(生成モード)】 私の依頼: 「挨拶をするPythonスクリプトと、その説明を書いたREADMEファイルを作って ##SCAFFOLD_GENERATE##」

あなたの期待される回答形式:

def greet(name):
  print(f"Hello, {name}!")

if __name__ == "__main__":
  greet("World")
# 簡単な挨拶プログラム

これは `hello.py` という名前のPythonスクリプトです。
実行すると挨拶メッセージを表示します。

## 使い方

```bash
python hello.py
```

## コード例

```python
def greet(name):
  print(f"Hello, {name}!")
```

【出力例(プランニングモード)】 私の依頼: 「簡単なブログシステムのファイル構成案を教えて ##SCAFFOLD_PLAN##」

あなたの期待される回答形式:

  • src/index.js - アプリケーションのエントリーポイント
  • src/components/PostList.js - 投稿一覧を表示するコンポーネント
  • src/components/PostDetail.js - 個別の投稿を表示するコンポーネント
  • src/services/api.js - API通信を行うモジュール
  • public/index.html - HTMLのベースファイル
  • README.md - プロジェクトの説明ファイル

これらの指示に従って、適切なモードで応答してください。


AIの思考プロセス

Assessing User Input

I'm currently focused on dissecting the user's instructions. I've successfully grasped the core concept: to identify specific trigger words in the user's last utterance. My primary objective now is to accurately interpret these triggers to transition into the relevant response mode.

Defining Response Modes

I've understood the user's instructions and identified the two key trigger words: ##SCAFFOLD_GENERATE## and ##SCAFFOLD_PLAN##. My task now is to switch response modes accordingly. Each mode has its own stringent output format rules, which I'm currently internalizing. I'm focusing on the precise formatting requirements, like the file path conventions and markdown code block usage, for both modes.

Clarifying Mode Logic

I've thoroughly analyzed the detailed format rules for both the scaffold generation and planning modes. I've focused on internalizing the distinct structures. The ##SCAFFOLD_GENERATE## mode requires file path-content pairs within Markdown code blocks. Conversely, the ##SCAFFOLD_PLAN## mode uses a simpler format with file paths and descriptions. I'm now confident in identifying the triggers and formatting the responses accurately.

AI: はい、承知いたしました。

あなたの指示に従い、最後の発言に含まれるトリガーワードに応じて、適切なモード(scaffold生成モード、プランニングモード、または通常対話モード)で応答します。

各モードの出力形式ルールも理解しました。特に、scaffold生成モードでの4つのバッククォートとファイルパスの指定、プランニングモードでの箇条書きと説明の形式を遵守します。

ご指示ありがとうございます。いつでも準備はできていますので、ご用件をお話しください。

(返答終了理由: STOP)


ユーザー: 以下のようなgoのコードを考えてます。

  • プロジェクト概要:

    • 名称: goat
    • 種別: Go言語用コマンドラインツール
    • 目的: go generate と連携し、main.go のコードを書き換えることでコマンドラインインターフェースを自動生成する
  • コア機能・動作フロー:

    • main.go を入力として受け取る
    • Goのパーサーを利用して main.go のコードを解析する
    • 規約:
      • main() 関数が特定の関数(デフォルト名: run、オプションで変更可能)を呼び出すことを前提とする
      • run 関数のシグネチャ:
        • run(options) error (プロトタイプでまず対応)
        • run(ctx, options) error (将来的には対応)
    • run() 関数の引数 options の型定義を解析する
      • options はユーザー定義の構造体
        • 例: struct Options { Name string \env:"USER_NAME"` }`
        • フィールドのコメントはオプションのヘルプメッセージとして利用
        • フィールドがポインタ型の場合、非必須オプションとして扱う
    • 解析結果に基づいてコマンドラインオプション用の設定コードを生成する
    • 生成したコードで既存の main() 関数の記述(docstring含む)を上書きする
      • 上書き方法: パーサーで既存 main() 関数の範囲を特定し、ファイルコピーの過程で該当部分を新しいコードに差し替える
  • コマンドラインオプション自動生成の要件:

    • 必須フラグ (required) の設定 (ポインタ型でないフィールド)
    • 列挙型 (enum) のバリデーション
      • 実装方針:
        • goat パッケージにマーカー関数 goat.Enum[T](T[]):T[] を用意
        • ユーザーは Options 構造体の初期化を行う関数 (例: NewOptions()) 内で、このマーカー関数をenumとして扱いたいフィールドに対して使用する
        • goat コマンドはこのマーカー関数の呼び出しを検出し、引数で渡されたスライスをenumの選択肢として認識する
        • 型レベルの表現 (例: type directionOption = Enum[Directions]) はGoの制約上難しいため、このマーカー関数方式を採用
        • メリット: 補完が効き、タイポを防ぐ
    • デフォルト値 (default) の設定
      • 実装方針 (変更):
        • 以前の default() 関数案から変更
        • goat パッケージにマーカー関数 goat.Default(value) を用意
        • ユーザーは Options 構造体の初期化を行う関数内で、このマーカー関数をデフォルト値を設定したいフィールドに対して使用する
        • ヘルプメッセージにも影響を与える
    • 環境変数からの読み込み
      • 実装方針: Options 構造体のタグ (env:"ENV_VAR_NAME") で環境変数名を指定する
    • ヘルプメッセージの生成
      • 実装方針: Goのコメントを解析し、ヘルプメッセージに変換する
    • (検討中) 設定ファイルからの読み込み:
      • 構造体タグ (file:"config.json") などでJSON形式などの設定ファイルを読み込む機能
  • 技術的な制約・方針:

    • AST (Abstract Syntax Tree) を直接操作する
    • go run コマンドやリフレクション (reflect パッケージ) は使用しない
    • go/packages パッケージは、大規模プロジェクトでのパフォーマンス懸念から使用を避ける
    • 生成されるコードや参照するコードは、Goの型チェックが通る有効なものであること
    • 「サブセット言語」の実行:
      • 対象: Options 構造体を初期化する関数 (例: NewOptions()) の内部
      • 目的: goat.Enum()goat.Default() などのマーカー関数の呼び出しを検出し、オプション設定に利用する
      • 意味合い: Goの構文としてパース可能であり、その一部が goat コマンドによって特別な意味を持つものとして解釈・実行(runnable)される
      • サポートする構文 (runnableの意味で):
        • 値のリテラル
        • 変数の初期化
        • 代入
        • 関数呼び出し (ただし goat パッケージ内のマーカー関数呼び出しのみ)
      • サポートしない構文 (runnableの意味で):
        • 条件分岐 (if, switch)
        • ループ (for)
        • 各種定義 (type, func など、ただし Options の初期化関数自体は除く)
        • チャネル操作
  • ユーザー体験の目標 (Hello World):

    • ユーザーが run(options) error のような関数を定義し、Options 構造体を定義するだけで、コマンドラインツールとして利用可能になる状態。

AIの思考プロセス

Considering Design Details

I've been diving into the nitty-gritty of the goat CLI tool's design. The focus now is on robust error handling and flexible configuration management. Thinking about how to best structure the command-line flags and subcommands for a user-friendly experience.

Defining Tool's Workflow

I'm now fully engaged with the workflow for the goat CLI tool. Analyzing the relationship between go generate and the automated rewriting of main.go has provided insights into how users will interact with the tool. The key is crafting a seamless flow, focusing on parsing main() and run() signatures to determine options. Also, I'm thinking about making the generated code elegant.

Refining Code Generation

I'm now focusing on the finer points of code generation for the goat tool. Currently, I'm working to integrate the marker functions goat.Enum and goat.Default seamlessly. The idea is to make sure the code generates type-safe, user-friendly CLI flags based on these annotations, and to ensure environmental variables are properly supported. Considering how to include configuration files. I aim to create CLI tools with run(options) error and an Options struct!

Grasping the Project Scope

I'm now fully immersed in understanding the expansive design presented for the goat CLI tool. The plan to automate CLI generation by analyzing main.go, and seamlessly integrate with go generate, is clear. The focus on parsing main() and run() signatures, coupled with utilizing marker functions, is intriguing. It's ambitious.

Reflecting on Design's Complexity

I'm now fully immersed in the nuances of the goat project's detailed design. The ambition to interact with main.go through AST manipulation and integrate seamlessly with go generate is compelling. The concept of executing a "subset language" within the Options initializer, along with the avoidance of go/packages, presents an intriguing challenge. I'm contemplating potential parsing and error-handling strategies.

Delving Deeper Into Logic

My focus is on the intricate aspects of the goat CLI tool's logic. I'm exploring the nuances of main() function manipulation and how goat.Enum and goat.Default will be interpreted within the AST. I am curious about the error-handling strategy for the parser and generator, as well as the specific plan for go generate integration. The notion of a "subset language" within the Options initializer intrigues me; I'm trying to understand how the limited syntax set will allow users to sufficiently express their desired initializations, and how you will manage syntax errors. Also, I'm thinking about future considerations for run(ctx, options) error, particularly how context.Context will impact the CLI generation.

Synthesizing Initial Questions

I'm now focusing on a deep dive into the specifics of the goat project's design. The plan to automate CLI generation by analyzing main.go using AST manipulation and integrating seamlessly with go generate is compelling. The integration of "marker functions" and the execution of a "subset language" within the Options initializer present exciting challenges. I'm wondering about the parsing of main() functions and the strategy for handling potential user-code errors. I am especially interested in understanding the concrete approach for resolving the key challenges within this project.

Clarifying Implementation Details

I'm now diving into the finer points of how the goat tool will function. I'm focusing on clarifying some key design decisions, such as precisely how existing main() content will be managed during CLI generation and how the "subset language" is interpreted. Considering how the tool will parse marker functions, handle errors, and integrate with go generate is crucial for user-friendliness. I want to build a tool that will be extensible in the future.

Analyzing Design Challenges

I've been re-evaluating the complexities inherent in the goat project's design. The use of a "subset language" to initialize Options and the AST-based manipulation of main.go present significant challenges. I'm focusing on the potential error scenarios that may arise from user code, and I'm considering how to ensure the CLI generation process remains robust and user-friendly. I'm also thinking about the details of how the tool will interact with go generate.

AI: `goat` プロジェクトの詳細な設計、ありがとうございます!非常に野心的で興味深いプロジェクトですね。 `go generate` と連携して `main.go` を直接書き換え、ASTを駆使してCLIを自動生成するというアプローチ、特に `go/packages` を避けつつ型安全なコードを生成しようとする点は、技術的に挑戦的で素晴らしいと思います。

特に以下の点が印象的です:

  • ASTを直接操作し、既存の main() 関数を書き換えるという大胆な手法。
  • goat.Enumgoat.Default といったマーカー関数を導入し、「サブセット言語」として解釈することで、型安全性を保ちつつ柔軟なオプション設定を実現しようとしている点。
  • リフレクションや go run に頼らないという制約。

いくつか質問させてください。

  1. main() 関数の上書きについて:

    • 既存の main() 関数は、goat が生成するCLI処理コードで完全に置き換えられるという理解で正しいでしょうか?それとも、ユーザーが main() 内に記述した他のロジック(例えば、初期設定やロガーのセットアップなど)は保持されるのでしょうか?docstring も含めて上書きとあるので、完全に置き換わるイメージでしょうか。
  2. マーカー関数の検出と解釈:

    • Options 構造体を初期化する関数(例: NewOptions())内で使用される goat.Enumgoat.Default は、AST上でどのように特定し、その引数を解釈してCLIのオプション設定に反映させるのでしょうか?例えば、goat.Enum([]string{"a", "b"})[]string{"a", "b"} を正確に抽出する具体的な戦略があれば教えていただけますか?
  3. 「サブセット言語」の実行について:

    • この「サブセット言語」のパーサー/インタープリターは goat ツール内に独自に実装される形になるのでしょうか?(Goのパーサーを利用するが、その後の解釈部分を指しています)
    • サポートしない構文(if, forなど)が Options の初期化関数内に記述されていた場合、goat はエラーとして処理するのか、それとも無視するのか、どのような挙動を想定していますか?
  4. エラーハンドリング:

    • main.go のパースエラー、goat の規約に沿わないコード(例: run 関数が見つからない、Options 構造体の型が不正など)、コード生成時のエラーなど、様々なエラーケースが考えられますが、これらのエラーハンドリングやユーザーへのフィードバックはどのように行う予定ですか?
  5. go generate との連携:

    • go generate コメントは具体的にどのような形を想定していますか?例えば、//go:generate goat のようにシンプルにツールを呼び出す形でしょうか、それとも何か引数を渡す形(例: run 関数の名前を指定するなど)でしょうか?
  6. run(ctx, options) error への対応:

    • 将来的に context.Contextrun 関数が受け取るようにするとのことですが、この context はCLIツールとしてどのように活用されることを想定していますか?例えば、タイムアウトやキャンセルシグナルの伝播などでしょうか。

これらの点が明確になると、goat の全体像がより深く理解できるかと思います。 ユーザーが最小限の記述で強力なCLIツールを生成できるという目標は非常に魅力的ですね。開発の進捗を楽しみにしています!

(返答終了理由: STOP)


ユーザー:

main() 関数の上書きについて: 置き換えられます。ロガーの設定はrun()の中でやってください。

マーカー関数の検出と解釈:

リテラルで渡されるならそれが使われる。関数と言っても直接sliceを返すようなものならそこもパースして値を取ってくる(つまり擬似的なimportも対応する)

「サブセット言語」の実行について:

はじめはパーサーとastはgoパッケージ配下のものを使う予定。インタプリタは自作。

サポートしない構文がでたらエラーを出して終了。

エラーハンドリング

エラーメッセージをテキトーに出して終了(まだ雑で良い)

go generate との連携

シンプルには go:generate goat main.go フルで記述すると go:generate -run run -default default -main main main.go


AIの思考プロセス

Grasping Project Goals

I've made significant strides in understanding the user's goat project based on their feedback. The main focus has been the complete replacement of the main() function. This is key for understanding the system's architecture.

Defining Implementation Scope

I've clarified the primary tasks involved in the goat project, especially regarding the complete substitution of the main() function with a run() function. I now understand the need to handle marker function interpretations, extracting values from both literals and simple function calls. This includes the execution of the "subset language" using a custom interpreter via Go's standard parser/AST. I also considered basic error handling.

Outlining Next Steps

My understanding has expanded significantly regarding the goat project, incorporating the detailed user feedback. I recognize the focus on a run() function in place of main(), which simplifies the project's architecture. I now have a clearer grasp of the nuances of interpreting marker functions, error handling, and go generate integration. To proceed, I believe the next logical step involves solidifying a plan or exploring code generation for select functionalities. Would the user prefer a scaffolding plan or a prototype?

Refining Project Strategy

I've greatly deepened my understanding of the goat project based on the user's detailed insights. I've noted the emphasis on replacing main() with run(), the complexities of marker function interpretation, and the use of a custom interpreter via Go's standard tools. The inclusion of basic error handling and integration with go generate is also clear. Considering these points, what specific areas or approaches would the user like to explore next? Perhaps a scoping session to solidify a plan? ##PLANNING_SESSION##

AI: 詳細なご回答ありがとうございます!`goat` プロジェクトの設計思想や技術的なアプローチについて、より深く理解できました。
  • main() 関数の完全置換: 了解しました。run() 関数がアプリケーションロジックの中心となるわけですね。
  • マーカー関数の解釈: リテラルだけでなく、値を返す単純な関数呼び出しも解析対象とするというのは、非常に強力ですね。これにより、ユーザーはより柔軟に Options 構造体を初期化できそうです。
  • 「サブセット言語」の実行: Goの標準パーサー/ASTを活用しつつ、必要な部分に特化したインタープリターを自作するというアプローチは現実的で良いと思います。未サポート構文でのエラー終了も明確で助かります。
  • エラーハンドリング: 初期段階ではシンプルなエラーメッセージで問題ないとのこと、承知しました。
  • go generate との連携:
    • シンプルな //go:generate goat main.go
    • 詳細な設定が可能な //go:generate goat -run runFunctionName -default defaultFunctionName -main mainFunctionName main.godefaultFunctionNamemainFunctionName は、おそらく Options の初期化関数名や、対象となる main 関数を指すという理解で合っていますでしょうか? -runrun 関数の名前を指定するオプションですね。)

これらの情報を踏まえて、次にどのようなステップに進みましょうか?

例えば、以下のようなことが考えられます。

  1. ファイル構成案の作成: プロジェクトの骨子となるファイル構成をプランニングモードで提案する。 もしよろしければ、「goat プロジェクトの基本的なファイル構成案を教えて ##SCAFFOLD_PLAN##」のように、最後に ##SCAFFOLD_PLAN## を付けてお声がけください。
  2. 特定のコア機能の雛形生成: 例えば、main.go をパースして run 関数や Options 構造体を見つける部分、あるいはマーカー関数を検出するロジックの骨子など、特定の機能に関するコードの雛形を生成モードで作成する。 もしよろしければ、「goatmain.go パーサー部分の雛形を作って ##SCAFFOLD_GENERATE##」のように、最後に ##SCAFFOLD_GENERATE## を付けてお声がけください。
  3. さらに詳細な仕様検討: 特定の機能(例えば、enumのバリデーション生成ロジックや、環境変数タグの処理など)について、さらに詳細な設計や考慮事項を議論する。

もちろん、上記以外にご希望があればお気軽にお申し付けください。

(返答終了理由: STOP)


ユーザー: とりあえずヘルプメッセージの完成をインタプリタの実行の最初のステップとして進めていきましょう。

今の感じの要望でとりあえず雑にヘルプメッセージの例を挙げてください。


AIの思考プロセス

Exploring Goat's Help

I'm currently focused on constructing a helpful message for the goat command. My initial work involves identifying the essential elements for this help message, ensuring it's comprehensive yet concise. I'm aiming for clarity in describing the command's functionality, options, and expected inputs.

Refining Help Message Focus

I'm now zeroing in on the specific requirements for a help message generated by a goat-built command. My focus is on ensuring the inclusion of all necessary elements: tool name and description, usage instructions, available options (with detailed explanations, types, defaults, and environment variable information), any subcommands, and additional helpful context. Currently, I'm verifying the inclusion of these elements based on the goat framework, aiming for a structured and easily understandable presentation.

Analyzing Help Message Structure

I'm now diving into the structure of a help message generated by the goat framework for a user-defined command. I'm focusing on how to represent the user's Options struct effectively. My goal is to clearly and concisely display each option, including its name (short and long), description (from the field comments), type, default value (if applicable), and environmental variable information, while highlighting that some optional parameters are also required. I am currently considering how to deal with complex data structures such as enums and slices.

Drafting a Help Example

Now, I'm composing a sample help message based on the provided Options struct. My focus is on emulating the typical CLI library formats in Go, aiming for clarity and ease of understanding. I'm incorporating the information from the struct's field comments, the env tags, and the goat.Default and goat.Enum calls. Currently, I'm working on how to present enums and slices effectively within the help message.

Designing a Sample Help

Now, I'm working on crafting a sample help message based on the hypothetical Options struct and the NewOptions function. I'm focusing on emulating the typical help message structure used by Go CLI libraries. I am also incorporating the environmental variable information provided by env tags. The example aims to cover required and optional arguments, default values, and the enum functionality, ensuring a helpful and understandable message for users of a command generated by goat.

Formulating Help Message Design

I'm now devising the structure of the help message that goat would generate, based on the provided Options struct. My design incorporates best practices from established Go CLI libraries. Specifically, I am focusing on how to represent the goat.Default and goat.Enum functions' information within the help message and how to clearly differentiate required and optional flags, like name and age. I'll test the structure with the Options struct example.

Formulating a Helpful Example

I'm now generating a help message example based on the provided Options struct and the NewOptions function, emulating the typical CLI library format in Go. My focus is on representing the goat.Default and goat.Enum calls, including their combined use. I'm ensuring clear distinction between required and optional parameters, along with their associated environmental variables.

Formulating Final Help Message

After some reflection, I've finalized the structure and content for the help message. The representation incorporates both the goat.Default and goat.Enum calls, clearly distinguishes between required and optional arguments, and displays environmental variable information. It adheres to a standard Go CLI library format for enhanced readability. I've also added a note to clarify the distinction between required and optional parameters.

Refining the Help Example

I'm now iterating on the help message, refining its structure and content based on best practices from Go CLI libraries, keeping readability in mind. I'm focusing on clearly distinguishing between required and optional parameters, displaying environmental variables, and effectively representing the goat.Default and goat.Enum calls. Currently, I'm aiming for clarity in formatting the output.

Formulating Help Output

I'm now devising the final form of the help message that goat will produce. My goal is to craft a readable and informative output, adhering to the structure used by common Go CLI libraries. I've decided to prioritize a clear presentation of required and optional flags, environmental variables, default values, and the enum functionality, all while maintaining a user-friendly format. The goal is to provide a "good enough" representation to the user.

Formulating Final Help

I've completed a functional example of a help message generated by the goat tool, now. This example incorporates all the required elements: a concise description, usage instructions, comprehensive flag details (name, description, type, default, environmental variable, and enum constraints), and a clear distinction between required and optional parameters. The presentation is designed to mirror standard Go CLI library formats for ease of understanding. I plan on testing this structure.

Formulating Final Help

I've crafted a functional example help message, now. It incorporates the goat.Default and goat.Enum calls, clearly distinguishes required and optional arguments, and displays environmental variables. It mirrors standard Go CLI library formats for readability, adding a note clarifying required parameters. This test case looks solid.

I've now finalized the structure and content for the help message. The representation incorporates both the goat.Default and goat.Enum calls, clearly distinguishes between required and optional arguments, and displays environmental variable information, while adhering to a standard Go CLI library format. I've also added a note to clarify the distinction between required and optional parameters.

I've now finalized the structure and content for the help message. The representation incorporates both the goat.Default and goat.Enum calls, clearly distinguishes between required and optional arguments, and displays environmental variable information. It adheres to a standard Go CLI library format for enhanced readability. I've also added a note to clarify the distinction between required and optional parameters.

Crafting a Final Help Example

Now, I'm integrating everything into a final example. I've considered formatting, clarity, and the efficient display of information from goat.Default and goat.Enum. This final version prioritizes a readable and informative representation of the help message that goat will generate, closely mirroring standard Go CLI formats. It's ready for final evaluation.

AI: はい、承知いたしました。 `goat` がユーザーの `main.go` 内の `Options` 構造体やマーカー関数(`goat.Default`, `goat.Enum`)を解析して生成するヘルプメッセージの「雑な例」ですね。

以下のようなユーザーコードを想定してみましょう。

// main.go (ユーザーが書くコードの抜粋)

package main

import "github.com/your_org/goat/pkg/goat" // goatパッケージの仮のパス

// Options defines the command line options for this example tool.
// This tool demonstrates how goat can generate a CLI.
type Options struct {
	// Name of the person to greet.
	// This is a mandatory field.
	Name string `env:"GREETER_NAME"`

	// Age of the person. Optional.
	Age *int `env:"GREETER_AGE"`

	// LogLevel for the application.
	// Can be one of: debug, info, warning, error.
	LogLevel string `env:"GREETER_LOG_LEVEL"`

	// Features to enable, comma-separated.
	Features []string

	// OutputDir for generated files.
	// Defaults to "out" if not specified.
	OutputDir string

	// Mode of operation for the tool.
	Mode string `env:"GREETER_MODE"`
}

// NewOptions initializes Options with defaults and enum constraints.
func NewOptions() *Options {
	return &Options{
		LogLevel:  goat.Default("info", goat.Enum([]string{"debug", "info", "warning", "error"})),
		OutputDir: goat.Default("out"),
		Mode:      goat.Enum([]string{"standard", "turbo", "eco"}),
	}
}

// run is the main logic for the CLI tool.
func run(opts Options) error {
	// ... application logic using opts ...
	return nil
}

func main() {
	// This will be replaced by goat
}

上記コードを goat が処理した場合、生成されるコマンドラインツールのヘルプメッセージは、例えば以下のようになるでしょう(コマンド名は仮に mytool とします)。

mytool - Options defines the command line options for this example tool.
         This tool demonstrates how goat can generate a CLI.

Usage:
  mytool [flags]

Flags:
  --age int                Age of the person. Optional.
                           (env: GREETER_AGE)
  --features strings       Features to enable, comma-separated.
  -h, --help               Show this help message and exit
  --log-level string       LogLevel for the application.
                           Can be one of: debug, info, warning, error.
                           (default: "info") (env: GREETER_LOG_LEVEL)
                           (allowed: "debug", "info", "warning", "error")
  --mode string            Mode of operation for the tool.
                           (env: GREETER_MODE)
                           (allowed: "standard", "turbo", "eco")
  --name string            Name of the person to greet.
                           This is a mandatory field. (required) (env: GREETER_NAME)
  --output-dir string      OutputDir for generated files.
                           Defaults to "out" if not specified. (default: "out")

このヘルプメッセージのポイント:

  • ツールの説明: Options 構造体のコメントが利用されています。
  • 必須フラグ: Name フィールド(ポインタ型でない)は (required) と表示されています。
  • オプションフラグ: Age フィールド(ポインタ型)は特に注釈がなく、任意であることが示唆されます。
  • デフォルト値: LogLevelOutputDirgoat.Default() で指定された値が (default: "...") として表示されます。
  • Enum(選択肢): LogLevelModegoat.Enum() で指定された値が (allowed: "...") として表示されます。
  • 環境変数: 各フィールドの env タグで指定された環境変数名が (env: ...) として表示されます。
  • ヘルプテキスト: 各フィールドのコメントが、そのフラグの説明として利用されます。複数行コメントもそのまま反映されるイメージです。
  • フラグ名: フィールド名(例: LogLevel)はケバブケース(例: --log-level)に変換されています。
  • 標準ヘルプ: -h, --help は自動的に追加されます。

このようなヘルプメッセージが自動生成されることで、ユーザーは Options 構造体の定義と簡単な初期化関数を書くだけで、情報豊富なCLIツールを手軽に作れるようになる、というイメージです。

「雑に」とのことでしたので、フォーマットの細部よりも、どのような情報がどのようにヘルプメッセージに反映されるか、という点が伝われば幸いです。 この例を元に、インタプリタの実装を進めていく上での具体的なイメージを掴んでいただければと思います。

(返答終了理由: STOP)


ユーザー: 良いですね。runのドキュメントコメントをコマンド自体のヘルプメッセージに使おうと思います。


AIの思考プロセス

Prioritizing Documentation Generation

I'm now focused on using the run function's documentation comments as the main source for the overall help message. This shifts the focus from the initial suggestion of using the Options struct's comments. I believe this will yield a clearer and more comprehensive description for users.

Refining the Help System Design

I'm now thinking about how to best integrate the user's desire to use the run function's documentation comments for the overall help message. This is a shift from my earlier suggestion. I believe a planning phase is appropriate, allowing me to sketch out a file structure and initial content for the help message generation, including considerations like AST analysis, marker function interpretation, and help text formatting.

Drafting a File Structure

I've revised my approach to align with the user's desire to leverage the run function's documentation comments for the help message. I'm leaning toward a planning phase now, rather than immediately generating code. I'm focusing on proposing a file structure and outlining the responsibilities of each component. This includes main.go, parser.go, interpreter.go, help_generator.go, and code_generator.go. I believe this will allow for a more streamlined and accurate approach to achieving the goal.

Defining Help Generation Steps

I'm now focusing on a more granular breakdown of the help message generation process, driven by the user's focus on utilizing the documentation comments of the run function for the overall help message. I'm prioritizing the refinement of the internal/generator/help_generator.go, internal/parser/parser.go, and internal/interpreter/interpreter.go modules, ensuring a clear and effective pipeline for help text extraction and formatting. The initial plan will be a planning phase to provide a more detailed structure.

Structuring for Help Integration

I'm now fully onboard with the user's focus on utilizing the run function's documentation comments for the help message. I'm prioritizing a planning phase to design a robust file structure, including main.go, internal/parser/parser.go, and internal/generator/help_generator.go, with a specific focus on the roles of internal/generator/help_generator.go, internal/parser/parser.go, and internal/interpreter/interpreter.go. I believe this will provide a solid foundation for help message generation.

Structuring for Documentation Integration

I've taken the user's feedback to heart and am now concentrating on a plan to integrate the run function's documentation comments into the overall help message. This shifts the focus from the previous suggestion of using the Options struct's comments. I believe this aligns better with a user-centric approach. I'm focusing on the plan for internal/generator/help_generator.go and related modules to ensure that the user's desire is fulfilled.

AI: なるほど!`run` 関数のドキュメントコメントをコマンド全体のヘルプメッセージ(ツールの説明部分)に利用するというのは、とても良いアイデアですね。 コードとドキュメントの対応がより直接的になり、ユーザーにとっても管理しやすそうです。`godoc` と同じような感覚で書けるのはメリットが大きいですね。

この方針を反映すると、前回のヘルプメッセージ例の先頭部分は以下のようになるイメージですね。

// main.go (ユーザーが書くコードの抜粋)

package main

// (略) Options構造体定義など

// run is the main logic for the CLI tool.
// This tool demonstrates how goat can generate a CLI.
// It takes several options to configure its behavior.
// For example, you can specify a name and an age.
func run(opts Options) error {
	// ... application logic using opts ...
	return nil
}

// (略)

↓ 生成されるヘルプメッセージの先頭

mytool - run is the main logic for the CLI tool.
         This tool demonstrates how goat can generate a CLI.
         It takes several options to configure its behavior.
         For example, you can specify a name and an age.

Usage:
  mytool [flags]

Flags:
  (以下略)

このように、run 関数のコメントがツールの概要説明として表示されるわけですね。

承知いたしました。この方針で、ヘルプメッセージ生成機能の具体的な実装に向けて進めていきましょう。

次にどのような情報交換や作業を行いましょうか? 例えば、このヘルプメッセージ生成ロジックを含む初期のファイル構成案をプランニングモードで検討したり、あるいは特定の処理(run 関数のコメント抽出、フィールドのタグやコメントの解析など)のプロトタイプコードを生成モードで作成したりすることも可能です。

お気軽にご指示ください!

(返答終了理由: STOP)


ユーザー: ##SCAFFOLD_PLAN## とりあえずのインタプリタの目的はヘルプメッセージの作成ですがメタデータの収集やコードの解析は真面目にやってください。そしてメタデータの収集をしてからそれをヘルプメッセージに反映するという形にしてください(あとでコードを出力する処理を追加したいので)。 横着せず全力を出してください。


AIの思考プロセス

Initiating File Structure

I'm currently focused on the file organization, aiming for a clear and logical structure. I'm putting together a draft of a file plan, with each entry accompanied by a brief explanation. My goal is to outline all the essential parts and reasons for their inclusion, preparing for the next phase.

Clarifying Core Goals

My primary focus is solidifying the project's purpose and key functionalities, particularly the automatic CLI generation using go generate. I'm starting to sketch out the interactions with the abstract syntax tree and parsing the run(options) error signature. Crucially, I'm working to interpret the Options struct, including field comments, env tags, and pointers. I'm also planning the implementation of helper message generation.

Refining Core Functionality

I'm now diving deeper into the nuances of parsing Options structs. I'm focusing on the interplay between field comments, env tags, and pointers, as this directly impacts the help message generation. The goal is to create a robust system that accurately represents the structure of user-defined commands. I'm also considering the implications of goat.Enum and goat.Default within the initialization function.

Elaborating the File Structure

Now I'm delving into the specifics of the file structure. I'm focusing on creating an initial, and comprehensive draft of the file organization, as described in the plan, including the key directories and their respective roles. I'm actively considering the placement of utility functions and the necessary test files to support the various modules. I'm working to produce a robust scaffold with a clear division of concerns.

Structuring the Blueprint

I'm now focusing on a file structure proposal. I've sketched out the initial directory and file plan, with each entry accompanied by a concise explanation. My goal is a comprehensive directory structure that supports the automatic CLI generation features. I'm exploring the implications of including goat.Enum and goat.Default within the interpreter, and the role of the metadata structures. I'm aiming for a robust foundation to expand functionality in future iterations.

Outlining the Parser

I've been working on the parsing structure, focusing on a detailed plan that outlines the key functionalities. The parser will handle the crucial tasks of scanning the main.go file, identifying and parsing the run function signature, and extracting the comments and annotations from the Options struct. My intention is to ensure the parsing is comprehensive and efficient, a key first step for help message generation. The ##SCAFFOLD_PLAN## trigger triggered a file structure plan. This includes entry points, parser, interpreter, metadata structures, help and code generators, and necessary utils and tests.

Constructing the Foundation

I'm now fully immersed in solidifying the initial file structure, which will serve as the backbone for the auto-CLI generation tool. I've broken down each part of the plan into concise descriptions. The directory structure is now mapped out, with a detailed breakdown of each file's role and its necessity. I'm focusing on creating an extensible architecture to facilitate future code generation features. I'm taking into consideration the function of goat.Enum and goat.Default within the interpreter and the overall structure of the metadata objects.

Structuring the Foundation

I've been working on the file structure, outlining key components and their functions. I've detailed the role of each directory and file. I'm focusing on parsing the Options struct, including field comments, tags, and pointers. Crucially, I'm interpreting the initializer's subset language. The structure is meant to facilitate future code generation, integrating metadata for robust help generation. I'm taking the function of goat.Enum and goat.Default within the interpreter and the overall structure of the metadata objects into consideration.

AI: はい、承知いたしました。ヘルプメッセージ生成を最初のステップとしつつ、将来のコード生成も見据えた本格的なメタデータ収集とコード解析を行うためのファイル構成案ですね。「横着せず全力で」とのこと、拡張性と責務分離を意識した構成を提案します。

以下に、goat プロジェクトのファイル構成案と各ファイルの役割をプランニングモードで提示します。

  • cmd/goat/main.go - goat コマンドラインツールのエントリーポイント。コマンドライン引数の解析、主要な処理フローの呼び出し、結果の出力を行います。
  • internal/config/config.go - goat コマンド自体の設定(例: -run で指定される関数名など)を管理します。
  • internal/loader/loader.go - 指定されたGoファイルをロードし、AST (Abstract Syntax Tree) を構築する責務を持ちます。go/parser を利用します。
  • internal/analyzer/analyzer.go - ASTを巡回し、プロジェクトの規約(run関数の存在、Options構造体の形式など)に基づいて情報を抽出・検証するコアロジック。
  • internal/analyzer/run_func_analyzer.go - run関数(または設定で指定されたターゲット関数)のシグネチャ、ドキュメントコメントなどを解析し、metadata.Command 構造体に格納します。
  • internal/analyzer/options_analyzer.go - run関数の引数である Options 構造体の定義を解析します。フィールド名、型、コメント、タグ(envなど)を抽出し、metadata.Option のリストを作成します。
  • internal/analyzer/initializer_analyzer.go - Options 構造体を初期化する関数(例: NewOptions)を特定し、その関数内のASTを解析する準備をします。この情報もメタデータに含めます。
  • internal/interpreter/interpreter.go - Options 初期化関数のASTを「サブセット言語」として解釈実行します。goat.Default()goat.Enum() といったマーカー関数の呼び出しを検出し、その引数(デフォルト値、enumの選択肢)を評価・抽出し、対応する metadata.Option を更新します。
  • internal/interpreter/evaluator.go - インタプリタがマーカー関数の引数(リテラル、定数、単純なスライス/マップリテラルなど)を評価するためのロジック。
  • internal/metadata/types.go - 解析・解釈された情報を格納するための中間データ構造を定義します。例: CommandMetadata(コマンド名、説明、run関数情報)、OptionMetadata(オプション名、型、ヘルプテキスト、必須フラグ、デフォルト値、enum候補、環境変数名など)。このメタデータがヘルプ生成やコード生成の基礎となります。
  • internal/help/generator.go - metadata.CommandMetadatametadata.OptionMetadata のリストを元に、整形されたヘルプメッセージ文字列を生成します。
  • internal/codegen/main_generator.go - (将来の機能) metadata を元に、CLI処理ロジックを含む新しい main() 関数のGoコードを生成します。
  • internal/codegen/writer.go - (将来の機能) 生成されたコードを既存の main.go に書き込む(あるいは新しいファイルを生成する)処理を担当します。既存の main() 関数の範囲を特定し、差し替えるロジックを含みます。
  • internal/utils/astutils.go - AST操作に関するユーティリティ関数群(例: 特定のノードの検索、コメントの抽出、リテラル値の取得など)。
  • internal/utils/stringutils.go - 文字列操作に関するユーティリティ関数群(例: キャメルケースからケバブケースへの変換など)。
  • pkg/goat/markers.go - ユーザーが自身のコード (main.go) 内で import して使用するマーカー関数 goat.Enum[T](values []T) []Tgoat.Default[T](value T, enumConstraint ...[]T) T などを定義します。これらの関数の実体は解析時には重要ではなく、シグネチャや呼び出しパターンが解析の手がかりとなります。
  • go.mod - Goモジュールの定義ファイル。
  • README.md - goat プロジェクト自体の説明、ビルド方法、使用方法、コントリビューションガイドなどを記述します。
  • examples/simple/main.go - goat を使った簡単なサンプルコード。テストや動作確認、ドキュメント用。
  • examples/simple/Makefile - (オプション) go generate を実行するための簡単なMakefile。

この構成案では、各コンポーネントの責務を明確に分離し、メタデータの収集・解析からヘルプ生成(そして将来のコード生成)への流れを意識しています。特に analyzerinterpreter を分けることで、ASTからの情報抽出と、その情報に基づいた動的な値の解釈という異なる関心事を扱えるようにしています。 pkg/goat/markers.go はユーザーが直接触れるAPIとなるため、internal 外に配置しています。

(返答終了理由: STOP)


ユーザー: pkg/goat/markers.go これはトップレベルのgoatパッケージにしてください。

あ、フルのパッケージパスはgithub.com/podhmo/ goatです。

##SCAFFOLD_GENERATE##


AIの思考プロセス

Restructuring Package Locations

I've decided to move the markers.go file from the pkg/goat directory to the top level, renaming it to goat/markers.go. This simplifies the package structure. Furthermore, I've confirmed that the project's root path will be github.com/podhmo/goat.

Defining Project Structure

I've implemented the requested modifications to the file structure. I've moved markers.go to the project's root and updated the import paths. Next, I'm working on generating the initial code. Specifically, the go.mod file with github.com/podhmo/goat as the module name and a main program in cmd/goat. I'll also add goat/markers.go, including Enum and Default markers.

Generating Initial Code

I've successfully implemented the requested file structure modifications. I've moved markers.go and confirmed the module name in go.mod. Now, I'm concentrating on generating initial code for the core components. I'm building out the go.mod file, a basic main.go in cmd/goat, and the marker definitions in goat/markers.go. I'm also starting to flesh out the data structures for storing the parsed metadata in internal/metadata/types.go and generating stubs for the loader and analyzer. This is all being done with the ##SCAFFOLD_GENERATE## trigger in mind, ensuring the code adheres to a predefined format.

Defining Initial Structures

I've revised the file structure and module path as requested, with goat/markers.go now in the project root. I'm focusing on the ##SCAFFOLD_GENERATE## trigger and laying the groundwork for the core components. I'm working on the go.mod, cmd/goat/main.go, and defining the Enum and Default markers. I've started building the data structures for storing parsed metadata in internal/metadata/types.go and defining the functions for the loader, analyzer, interpreter, and other key parts.

Refining Initial Structures

I'm focused on revising the structure based on the provided plan. The new top-level goat package and root path are now clearly defined. My work centers on generating skeletal code for key components with ##SCAFFOLD_GENERATE##. I'm building go.mod, cmd/goat/main.go, and the markers in goat/markers.go. I'm creating placeholder data structures in internal/metadata/types.go and defining the functions for the loader, analyzer, and interpreter, while also crafting a sample usage in examples/simple.

Constructing Core Components

I'm making progress in generating the initial scaffolding. I'm focusing on creating the essential files. Specifically, I'm working on the go.mod, a basic cmd/goat/main.go, and the definitions of the Enum and Default markers in goat/markers.go. I've also begun sketching the data structures for metadata storage in internal/metadata/types.go and the function stubs for loading, analyzing, and interpreting the code. I'm taking care to meet the ##SCAFFOLD_GENERATE## trigger requirements, ensuring alignment with project goals.

Initiating Core Code Generation

I'm focused on generating the initial code structure as planned. I'm prioritizing go.mod with the specified module path and a basic cmd/goat/main.go. Additionally, I'm working on the goat/markers.go, defining Enum and Default. I'm also building out the core data structures in internal/metadata/types.go and the associated functions for loading, analysis, interpretation, and help generation. The aim is to generate skeletal structures while being mindful of the ##SCAFFOLD_GENERATE## trigger.

Defining Initial Code Generation

I'm focused on generating the initial code structure with the updated file paths and module name. I'm building go.mod with github.com/podhmo/goat, a basic cmd/goat/main.go, and defining Enum and Default markers in goat/markers.go. I'm starting to build core data structures in internal/metadata/types.go, including a CommandMetadata, and the stub functions for the loader, analyzer, and interpreter, while keeping an eye on the ##SCAFFOLD_GENERATE## trigger.

Defining Core Component Structures

I'm focused on developing the initial skeletal code base, now adhering to the specified file paths and root module name. I've begun crafting the go.mod file, a basic cmd/goat/main.go, and the markers in goat/markers.go, including Enum and Default. I'm also building out the internal/metadata/types.go data structures, specifically CommandMetadata, while generating the foundational function stubs for loading, analyzing, interpreting, and help generation. The main objective is to establish a solid structure aligned with the ##SCAFFOLD_GENERATE## trigger criteria.

Constructing Initial Core

I've finalized the structure, focusing on generating key components within ##SCAFFOLD_GENERATE##. I'm building go.mod with the module path, a basic cmd/goat/main.go, and defining the Enum and Default markers. I'm also constructing core data structures in internal/metadata/types.go and the stub functions for loading, analyzing, and interpreting the code. The aim is to have a functional base.

Generating Core Structures

I've implemented the requested core structure changes, with the root package location now properly defined. I'm focused on generating the initial skeletal code based on the established file paths, including goat/markers.go. I'm building out go.mod, a basic cmd/goat/main.go, and the marker definitions. I'm also setting up placeholder data structures in internal/metadata/types.go, including CommandMetadata, and the stub functions for the loader, analyzer, and interpreter, while remaining mindful of the ##SCAFFOLD_GENERATE## trigger.

Initializing Code Generation

I'm now fully focused on initiating the code generation process according to the updated project structure and using the planned components. I'm focusing on defining core data structures within internal/metadata/types.go and the associated functions, including the CommandMetadata, while also building out the stubs for key function like the loader, analyzer, and interpreter, all with an eye toward the ##SCAFFOLD_GENERATE## trigger.

AI: はい、承知いたしました。 「`pkg/goat/markers.go` をトップレベルの `goat` パッケージにする」という点と、「フルのパッケージパスは `github.com/podhmo/goat` である」という点を反映し、ヘルプメッセージ生成を初期目標としつつ、将来のコード生成も見据えたメタデータ収集とコード解析のためのファイル群の雛形を生成します。
module github.com/podhmo/goat

go 1.20
package main

import (
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/podhmo/goat/internal/analyzer"
	"github.com/podhmo/goat/internal/config"
	"github.com/podhmo/goat/internal/help"
	"github.com/podhmo/goat/internal/interpreter"
	"github.com/podhmo/goat/internal/loader"
	// "github.com/podhmo/goat/internal/codegen" // Future use
)

func main() {
	// Define command-line flags for goat tool itself
	var (
		runFuncName            string
		optionsInitializerName string
		targetFilename         string
		// mainFuncName string // TODO: for specifying target main func name
	)

	flag.StringVar(&runFuncName, "run", "run", "Name of the function to be treated as the entrypoint (e.g., run(Options) error)")
	flag.StringVar(&optionsInitializerName, "initializer", "NewOptions", "Name of the function that initializes the options struct (e.g., NewOptions() *Options)")
	// TODO: add more flags for goat's configuration if needed

	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s [options] <target_gofile.go>\n\nOptions:\n", os.Args[0])
		flag.PrintDefaults()
	}
	flag.Parse()

	if flag.NArg() < 1 {
		fmt.Fprintln(os.Stderr, "Error: Target Go file must be specified.")
		flag.Usage()
		os.Exit(1)
	}
	targetFilename = flag.Arg(0)

	cfg := &config.Config{
		RunFuncName:            runFuncName,
		OptionsInitializerName: optionsInitializerName,
		TargetFile:             targetFilename,
	}

	if err := runGoat(cfg); err != nil {
		log.Fatalf("Error: %+v", err) // Use %+v for more detailed error from pkg/errors
	}
}

func runGoat(cfg *config.Config) error {
	log.Printf("Goat: Analyzing %s with runFunc=%s, optionsInitializer=%s", cfg.TargetFile, cfg.RunFuncName, cfg.OptionsInitializerName)

	// 1. Load and parse the target Go file
	fileAST, err := loader.LoadFile(cfg.TargetFile)
	if err != nil {
		return fmt.Errorf("failed to load target file %s: %w", cfg.TargetFile, err)
	}

	// 2. Analyze the AST to extract metadata
	// This step identifies the main command, run function, options struct, and its fields.
	cmdMetadata, optionsStructName, err := analyzer.Analyze(fileAST, cfg.RunFuncName)
	if err != nil {
		return fmt.Errorf("failed to analyze AST: %w", err)
	}
	log.Printf("Goat: Command metadata extracted for command: %s", cmdMetadata.Name)

	// 3. Interpret the options initializer function (e.g., NewOptions)
	// This step evaluates goat.Default() and goat.Enum() calls to populate default values and enum choices.
	if cfg.OptionsInitializerName != "" && optionsStructName != "" {
		err = interpreter.InterpretInitializer(fileAST, optionsStructName, cfg.OptionsInitializerName, cmdMetadata.Options, "github.com/podhmo/goat/goat") // Pass marker package path
		if err != nil {
			return fmt.Errorf("failed to interpret options initializer %s: %w", cfg.OptionsInitializerName, err)
		}
		log.Printf("Goat: Options initializer interpreted successfully.")
	} else {
		log.Printf("Goat: Skipping options initializer interpretation (initializer name or options struct not found/specified).")
	}


	// 4. Generate help message
	helpMsg := help.GenerateHelp(cmdMetadata)
	fmt.Println("-------------------- Generated Help Message --------------------")
	fmt.Println(helpMsg)
	fmt.Println("----------------------------------------------------------------")


	// 5. TODO: Generate new main.go content (Future Step)
	// newMainContent, err := codegen.GenerateMain(cmdMetadata)
	// if err != nil {
	// 	return fmt.Errorf("failed to generate new main.go content: %w", err)
	// }

	// 6. TODO: Write the new content (Future Step)
	// For now, just print the target path where it would be written or how it would be modified
	// err = codegen.WriteMain(cfg.TargetFile, fileAST, newMainContent, cmdMetadata.MainFuncPosition)
	// if err != nil {
	//	return fmt.Errorf("failed to write modified main.go: %w", err)
	//}

	log.Println("Goat: Processing finished.")
	return nil
}
// Package goat provides marker functions used by the `goat` tool
// to understand user intentions for CLI option generation.
// These functions are intended to be used in the user's `main.go` file
// within the Options struct's initializer function (e.g. NewOptions).
// The `goat` tool parses the AST and identifies calls to these functions
// to extract default values, enum choices, etc.
// These functions themselves have minimal runtime behavior, typically
// just returning their input, as their primary purpose is static analysis.
package goat

// Enum marks a field as having a set of allowed values.
// The `goat` tool's interpreter will extract these `values`.
// It is used for analysis purposes only and returns the passed `values` as is at runtime.
// Type parameter T can be any type, but typically string, int, or other simple types
// suitable for command-line options.
func Enum[T any](values []T) []T {
	return values
}

// Default sets a default value for a field.
// The `goat` tool's interpreter will extract this `defaultValue`.
// It can optionally take an `enumConstraint` which is typically the result of a call to `Enum()`.
// If `enumConstraint` is provided and is a non-empty slice, its first element
// (which should be a slice of allowed values from `Enum()`) will be used for enum validation.
// It is used for analysis purposes only and returns the passed `defaultValue` as is at runtime.
// Type parameter T can be any type suitable for a default value.
func Default[T any](defaultValue T, enumConstraint ...[]T) T {
	// The enumConstraint argument is primarily for the static analyzer.
	// The analyzer will look for calls to goat.Enum() passed here.
	// At runtime, this function simply returns the defaultValue.
	return defaultValue
}
package config

// Config holds the configuration for the goat tool itself,
// typically derived from its command-line arguments.
type Config struct {
	RunFuncName            string // Name of the target 'run' function (e.g., "run")
	OptionsInitializerName string // Name of the options initializer function (e.g., "NewOptions")
	TargetFile             string // Path to the target Go file to be processed
	// MainFuncName string // TODO: Name of the main function if not "main"
	// OutputFile string // TODO: Path for the generated file, if not in-place
}
package loader

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

// LoadFile parses the given Go source file and returns its AST.
func LoadFile(filename string) (*ast.File, error) {
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("failed to parse file %s: %w", filename, err)
	}
	return file, nil
}
package analyzer

import (
	"fmt"
	"go/ast"

	"github.com/podhmo/goat/internal/metadata"
)

// Analyze inspects the AST of a Go file to extract command metadata,
// focusing on the run function and its associated options struct.
// It returns the main CommandMetadata, the name of the Options struct, and any error encountered.
func Analyze(fileAst *ast.File, runFuncName string) (*metadata.CommandMetadata, string /* optionsStructName */, error) {
	cmdMeta := &metadata.CommandMetadata{
		Options: []*metadata.OptionMetadata{},
	}
	var optionsStructName string

	// 1. Find the run function and extract its info (doc comment, params)
	runFuncInfo, runFuncDoc, err := AnalyzeRunFunc(fileAst, runFuncName)
	if err != nil {
		return nil, "", fmt.Errorf("analyzing run function '%s': %w", runFuncName, err)
	}
	cmdMeta.Name = fileAst.Name.Name // Use package name as initial command name, can be refined
	cmdMeta.Description = runFuncDoc
	cmdMeta.RunFunc = runFuncInfo

	// 2. If run function is found, analyze its Options struct
	if runFuncInfo != nil && runFuncInfo.OptionsArgName != "" && runFuncInfo.OptionsArgType != "" {
		options, foundOptionsStructName, err := AnalyzeOptions(fileAst, runFuncInfo.OptionsArgType, runFuncInfo.PackageName)
		if err != nil {
			return nil, "", fmt.Errorf("analyzing options struct for run function '%s': %w", runFuncName, err)
		}
		cmdMeta.Options = options
		optionsStructName = foundOptionsStructName
	} else {
		return nil, "", fmt.Errorf("run function '%s' or its options parameter not found or not in expected format", runFuncName)
	}
	
	// 3. TODO: Find the main function to get its position for future code replacement
	// mainFuncPos, err := FindMainFuncPosition(fileAst)
	// if err != nil {
	//    // Optional: main func might not exist if user is building a library part
	// }
	// cmdMeta.MainFuncPosition = mainFuncPos

	return cmdMeta, optionsStructName, nil
}
package analyzer

import (
	"fmt"
	"go/ast"
	"strings"

	"github.com/podhmo/goat/internal/metadata"
	"github.com/podhmo/goat/internal/utils/astutils"
)

// AnalyzeRunFunc finds the specified 'run' function in the AST and extracts its metadata.
// It returns the RunFuncInfo, the function's doc comment, and any error.
func AnalyzeRunFunc(fileAst *ast.File, funcName string) (*metadata.RunFuncInfo, string, error) {
	var runFuncDecl *ast.FuncDecl
	var docComment string

	ast.Inspect(fileAst, func(n ast.Node) bool {
		if fn, ok := n.(*ast.FuncDecl); ok && fn.Name.Name == funcName {
			runFuncDecl = fn
			if fn.Doc != nil {
				docComment = fn.Doc.Text()
			}
			return false // Stop searching
		}
		return true
	})

	if runFuncDecl == nil {
		return nil, "", fmt.Errorf("function '%s' not found", funcName)
	}

	info := &metadata.RunFuncInfo{
		Name:        runFuncDecl.Name.Name,
		PackageName: fileAst.Name.Name, // Assuming it's in the same package
	}

	// Analyze parameters: expecting `run(options MyOptions) error` or `run(ctx context.Context, options MyOptions) error`
	params := runFuncDecl.Type.Params.List
	if len(params) == 1 { // run(options MyOptions) error
		param := params[0]
		if len(param.Names) > 0 {
			info.OptionsArgName = param.Names[0].Name
		}
		info.OptionsArgType = astutils.ExprToTypeName(param.Type)
	} else if len(params) == 2 { // run(ctx context.Context, options MyOptions) error
		// TODO: Handle context parameter if necessary, for now assume 2nd is options
		ctxParam := params[0]
		optionsParam := params[1]

		if len(ctxParam.Names) > 0 {
			info.ContextArgName = ctxParam.Names[0].Name
		}
		info.ContextArgType = astutils.ExprToTypeName(ctxParam.Type)

		if len(optionsParam.Names) > 0 {
			info.OptionsArgName = optionsParam.Names[0].Name
		}
		info.OptionsArgType = astutils.ExprToTypeName(optionsParam.Type)
	} else {
		return nil, docComment, fmt.Errorf("function '%s' has unexpected signature: expected 1 or 2 parameters, got %d", funcName, len(params))
	}

	// TODO: Analyze return type (expecting `error`)

	return info, strings.TrimSpace(docComment), nil
}
package analyzer

import (
	"fmt"
	"go/ast"
	"reflect"
	"strings"

	"github.com/podhmo/goat/internal/metadata"
	"github.com/podhmo/goat/internal/utils/astutils"
	"github.com/podhmo/goat/internal/utils/stringutils"
)

// AnalyzeOptions finds the Options struct definition (given its type name)
// and extracts metadata for each of its fields.
func AnalyzeOptions(fileAst *ast.File, optionsTypeName string, packageName string) ([]*metadata.OptionMetadata, string /* actualStructName */, error) {
	var optionsStruct *ast.TypeSpec
	var actualStructName string

	// Remove package prefix if present (e.g. "main.Options" -> "Options")
	parts := strings.Split(optionsTypeName, ".")
	typeNameOnly := parts[len(parts)-1]


	ast.Inspect(fileAst, func(n ast.Node) bool {
		if ts, ok := n.(*ast.TypeSpec); ok {
			if ts.Name.Name == typeNameOnly {
				if _, isStruct := ts.Type.(*ast.StructType); isStruct {
					optionsStruct = ts
					actualStructName = ts.Name.Name
					return false // Stop searching
				}
			}
		}
		return true
	})

	if optionsStruct == nil {
		return nil, "", fmt.Errorf("options struct type '%s' not found in package '%s'", typeNameOnly, packageName)
	}

	structType, ok := optionsStruct.Type.(*ast.StructType)
	if !ok {
		// This should not happen if the previous check passed
		return nil, actualStructName, fmt.Errorf("type '%s' is not a struct type", actualStructName)
	}

	var extractedOptions []*metadata.OptionMetadata
	for _, field := fieldLoop := structType.Fields.List {
		if field.Names == nil || len(field.Names) == 0 {
			// Embedded struct, skip for now or handle later if needed
			continue fieldLoop
		}
		fieldName := field.Names[0].Name
		if !ast.IsExported(fieldName) {
			// Skip unexported fields
			continue fieldLoop
		}

		opt := &metadata.OptionMetadata{
			Name:       fieldName,
			CliName:    stringutils.ToKebabCase(fieldName),
			TypeName:   astutils.ExprToTypeName(field.Type),
			IsPointer:  astutils.IsPointerType(field.Type),
			IsRequired: !astutils.IsPointerType(field.Type), // Basic assumption: non-pointer is required
		}

		if field.Doc != nil {
			opt.HelpText = strings.TrimSpace(field.Doc.Text())
		}
		if field.Comment != nil {
			// Line comments might also be relevant, concatenate if necessary
			opt.HelpText = strings.TrimSpace(opt.HelpText + "\n" + field.Comment.Text())
			opt.HelpText = strings.TrimSpace(opt.HelpText)
		}


		if field.Tag != nil {
			tagStr := strings.Trim(field.Tag.Value, "`")
			tag := reflect.StructTag(tagStr)
			if envVar, ok := tag.Lookup("env"); ok {
				opt.EnvVar = envVar
			}
			// TODO: Add support for other tags like 'file', 'default', 'enum' if defined directly in tags
		}

		extractedOptions = append(extractedOptions, opt)
	}

	return extractedOptions, actualStructName, nil
}
package analyzer

// This file would contain logic to find and prepare the AST of the
// Options initializer function (e.g., NewOptions) for the interpreter.
// For now, it's a placeholder as the main analysis locates the function name,
// and the interpreter takes the AST and function name directly.

// FindOptionsInitializerFunc // Placeholder for a more dedicated finder if needed
// func FindOptionsInitializerFunc(fileAst *ast.File, funcName string) (*ast.FuncDecl, error) { ... }
package interpreter

import (
	"fmt"
	"go/ast"
	"go/token"
	"log"

	"github.com/podhmo/goat/internal/metadata"
	"github.com/podhmo/goat/internal/utils/astutils"
)

// InterpretInitializer analyzes the AST of an options initializer function (e.g., NewOptions)
// to extract default values and enum choices by "interpreting" calls to goat.Default() and goat.Enum().
// It modifies the passed cmdMetadata.Options directly.
func InterpretInitializer(
	fileAst *ast.File,
	optionsStructName string,
	initializerFuncName string,
	options []*metadata.OptionMetadata,
	markerPkgImportPath string, // e.g., "github.com/podhmo/goat/goat"
) error {
	var initializerFunc *ast.FuncDecl
	ast.Inspect(fileAst, func(n ast.Node) bool {
		if fn, ok := n.(*ast.FuncDecl); ok && fn.Name.Name == initializerFuncName {
			initializerFunc = fn
			return false
		}
		return true
	})

	if initializerFunc == nil {
		return fmt.Errorf("initializer function '%s' not found", initializerFuncName)
	}

	if initializerFunc.Body == nil {
		return fmt.Errorf("initializer function '%s' has no body", initializerFuncName)
	}

	// Map option field names to their metadata for quick lookup
	optionsMap := make(map[string]*metadata.OptionMetadata)
	for _, opt := range options {
		optionsMap[opt.Name] = opt
	}
	
	// TODO: This is a very simplified interpreter.
	// It should handle variable assignments that eventually set fields on the options struct.
	// For now, let's assume direct assignment like:
	// return &Options{ Field: goat.Default(...), ... }
	// or
	// opts := &Options{}
	// opts.Field = goat.Default(...)
	// return opts

	log.Printf("Interpreting initializer: %s", initializerFuncName)

	// Look for return statement like 'return &Options{...}'
	// or assignments like 'opt.Field = ...'
	ast.Inspect(initializerFunc.Body, func(n ast.Node) bool {
		switch stmtNode := n.(type) {
		case *ast.AssignStmt: // e.g. options.Field = goat.Default(...) or var x = goat.Default(...)
			// We need to trace assignments to see if they end up in an OptionMetadata field
			// This example focuses on direct assignments to struct fields.
			// E.g., `opt.MyField = goat.Default("value")`
			if len(stmtNode.Lhs) == 1 && len(stmtNode.Rhs) == 1 {
				if selExpr, ok := stmtNode.Lhs[0].(*ast.SelectorExpr); ok {
					// Assuming selExpr.X is the options struct variable, selExpr.Sel is the field name
					fieldName := selExpr.Sel.Name
					if optMeta, exists := optionsMap[fieldName]; exists {
						log.Printf("Found assignment to options field: %s", fieldName)
						extractMarkerInfo(stmtNode.Rhs[0], optMeta, fileAst, markerPkgImportPath)
					}
				}
			}

		case *ast.ReturnStmt: // e.g. return &Options{ Field: goat.Default(...) }
			if len(stmtNode.Results) == 1 {
				if compLit, ok := stmtNode.Results[0].(*ast.CompositeLit); ok {
					// Check if this composite literal is for our Options struct
					// This requires resolving compLit.Type to optionsStructName, which can be complex.
					// For a simpler start, assume if it's a struct literal in NewOptions, it's the one.
					log.Printf("Found return composite literal in %s", initializerFuncName)
					for _, elt := range compLit.Elts {
						if kvExpr, ok := elt.(*ast.KeyValueExpr); ok {
							if keyIdent, ok := kvExpr.Key.(*ast.Ident); ok {
								fieldName := keyIdent.Name
								if optMeta, exists := optionsMap[fieldName]; exists {
									log.Printf("Found key-value for options field in return: %s", fieldName)
									extractMarkerInfo(kvExpr.Value, optMeta, fileAst, markerPkgImportPath)
								}
							}
						}
					}
				}
			}
		}
		return true
	})

	return nil
}

// extractMarkerInfo extracts default value and enum choices from a marker function call.
func extractMarkerInfo(valueExpr ast.Expr, optMeta *metadata.OptionMetadata, fileAst *ast.File, markerPkgImportPath string) {
	callExpr, ok := valueExpr.(*ast.CallExpr)
	if !ok {
		// Value is not a function call, could be a direct literal (TODO: handle direct literals as defaults)
		return
	}

	markerFuncName, markerPkgAlias := astutils.GetFullFunctionName(callExpr.Fun)
	actualMarkerPkgPath := astutils.GetImportPath(fileAst, markerPkgAlias)
	
	if actualMarkerPkgPath != markerPkgImportPath {
		// log.Printf("Skipping call to %s.%s, not the target marker package (%s vs %s)", markerPkgAlias, markerFuncName, actualMarkerPkgPath, markerPkgImportPath)
		return
	}

	switch markerFuncName {
	case "Default":
		log.Printf("Interpreting goat.Default for field %s", optMeta.Name)
		if len(callExpr.Args) > 0 {
			// First arg is the default value
			optMeta.DefaultValue = astutils.EvaluateArg(callExpr.Args[0])
			log.Printf("  Default value: %v", optMeta.DefaultValue)

			// Subsequent args could be an Enum call for enumConstraint
			if len(callExpr.Args) > 1 {
				// Assume second arg is the enumConstraint, which might be a goat.Enum() call
				// or a slice literal.
				enumArg := callExpr.Args[1]
				if enumCallExpr, ok := enumArg.(*ast.CallExpr); ok {
					enumFuncName, enumPkgAlias := astutils.GetFullFunctionName(enumCallExpr.Fun)
					actualEnumPkgPath := astutils.GetImportPath(fileAst, enumPkgAlias)
					if actualEnumPkgPath == markerPkgImportPath && enumFuncName == "Enum" {
						if len(enumCallExpr.Args) == 1 {
							optMeta.EnumValues = astutils.EvaluateSliceArg(enumCallExpr.Args[0])
							log.Printf("  Enum values from goat.Enum: %v", optMeta.EnumValues)
						}
					}
				} else if _, ok := enumArg.(*ast.CompositeLit); ok { // Direct slice literal
					optMeta.EnumValues = astutils.EvaluateSliceArg(enumArg)
					log.Printf("  Enum values from slice literal: %v", optMeta.EnumValues)
				}
			}
		}
	case "Enum":
		log.Printf("Interpreting goat.Enum for field %s", optMeta.Name)
		if len(callExpr.Args) == 1 {
			optMeta.EnumValues = astutils.EvaluateSliceArg(callExpr.Args[0])
			log.Printf("  Enum values: %v", optMeta.EnumValues)
		}
	default:
		// Not a recognized marker function from the specified package
		// log.Printf("  Not a goat marker function: %s.%s", markerPkgAlias, markerFuncName)
	}
}
package interpreter

// This file would contain more sophisticated logic for evaluating expressions
// within the "subset language" of the Options initializer.
// For example, resolving variables, handling simple arithmetic for constants, etc.
// The astutils.EvaluateArg and astutils.EvaluateSliceArg provide a basic version of this.

// For now, astutils handles basic literal evaluation.
// Complex evaluations (e.g., var x = "foo"; opt.Val = x) would require a more
// stateful evaluator or symbol table within the interpreter.
package metadata

import "go/token"

// CommandMetadata holds all extracted information about a CLI command
// that goat needs to generate code or help messages.
type CommandMetadata struct {
	Name             string   // Name of the command (e.g., from package or explicitly set)
	Description      string   // Overall help description for the command (from run func doc)
	RunFunc          *RunFuncInfo
	Options          []*OptionMetadata
	MainFuncPosition *token.Position // TODO: For knowing where to replace main func content
}

// RunFuncInfo describes the target 'run' function.
type RunFuncInfo struct {
	Name           string // Name of the run function (e.g., "run")
	PackageName    string // Package where the run function is defined
	OptionsArgName string // Name of the options struct parameter (e.g., "opts")
	OptionsArgType string // Type name of the options struct (e.g., "Options", "main.Options")
	ContextArgName string // Name of the context.Context parameter (if present)
	ContextArgType string // Type name of the context.Context parameter (if present)
}

// OptionMetadata holds information about a single command-line option.
type OptionMetadata struct {
	Name         string // Original field name in the Options struct (e.g., "UserName")
	CliName      string // CLI flag name (e.g., "user-name")
	TypeName     string // Go type of the field (e.g., "string", "*int", "[]string")
	HelpText     string // Description for the option (from field comment)
	IsPointer    bool   // True if the field is a pointer type (often implies optional)
	IsRequired   bool   // True if the option must be provided
	EnvVar       string // Environment variable name to read from (from `env` tag)
	DefaultValue any    // Default value (from goat.Default or struct tag)
	EnumValues   []any  // Allowed enum values (from goat.Enum or struct tag)
	// FilePath string // TODO: For options loaded from a config file (`file` tag)
}
package help

import (
	"bytes"
	"fmt"
	"strings"
	"text/template"

	"github.com/podhmo/goat/internal/metadata"
)

const helpTemplate = `{{.CommandName}} - {{.CommandDescription}}

Usage:
  {{.CommandName}} [flags] {{.CommandArgsPlaceholder}}

Flags:
{{range .Options}}
  --{{.CliName}} {{.TypeIndicator}} {{.HelpText}}{{if .IsRequired}} (required){{end}}{{if .DefaultValue}} (default: {{.DefaultValue | QuoteIfString}}){{end}}{{if .EnvVar}} (env: {{.EnvVar}}){{end}}{{if .EnumValues}} (allowed: {{.EnumValues | JoinStrings ", "}}){{end}}
{{end}}
  -h, --help             Show this help message and exit
`

// FuncMap for the template
var funcMap = template.FuncMap{
	"QuoteIfString": func(v any) string {
		if s, ok := v.(string); ok {
			return fmt.Sprintf("%q", s)
		}
		return fmt.Sprintf("%v", v)
	},
	"JoinStrings": func(values []any, sep string) string {
		var s []string
		for _, v := range values {
			s = append(s, fmt.Sprintf("%v", v)) // QuoteIfString could be used here too if enums are strings
		}
		return strings.Join(s, sep)
	},
}


// GenerateHelp creates a formatted help message string from CommandMetadata.
func GenerateHelp(cmdMeta *metadata.CommandMetadata) string {
	if cmdMeta == nil {
		return "Error: Command metadata is nil."
	}

	type templateOption struct {
		CliName       string
		TypeIndicator string // e.g. "string", "int", "bool"
		HelpText      string
		IsRequired    bool
		DefaultValue  any
		EnvVar        string
		EnumValues    []any
	}

	var tplOptions []templateOption
	for _, opt := range cmdMeta.Options {
		tplOpt := templateOption{
			CliName:    opt.CliName,
			HelpText:   strings.ReplaceAll(opt.HelpText, "\n", "\n                           "), // Indent multi-line help
			IsRequired: opt.IsRequired,
			DefaultValue: opt.DefaultValue,
			EnvVar:     opt.EnvVar,
			EnumValues: opt.EnumValues,
		}
		// Simplify type indicator for help message
		baseType := strings.TrimPrefix(opt.TypeName, "*") // Remove pointer indicator for base type
		baseType = strings.TrimPrefix(baseType, "[]") // Remove slice indicator
		parts := strings.Split(baseType, ".")
		tplOpt.TypeIndicator = strings.ToLower(parts[len(parts)-1]) // Show simple type like "string", "int"
		if strings.HasPrefix(opt.TypeName, "[]") {
			tplOpt.TypeIndicator += "s" // e.g. strings, ints
		}


		tplOptions = append(tplOptions, tplOpt)
	}

	// Prepare data for the template
	data := struct {
		CommandName            string
		CommandDescription     string
		CommandArgsPlaceholder string // TODO: if command takes positional args
		Options                []templateOption
	}{
		CommandName:            cmdMeta.Name, // Or a more specific CLI executable name
		CommandDescription:     strings.ReplaceAll(cmdMeta.Description, "\n", "\n         "), // Indent multi-line desc
		CommandArgsPlaceholder: "", // Placeholder for now
		Options:                tplOptions,
	}

	tmpl, err := template.New("help").Funcs(funcMap).Parse(helpTemplate)
	if err != nil {
		return fmt.Sprintf("Error parsing help template: %v", err)
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		return fmt.Sprintf("Error executing help template: %v", err)
	}

	return buf.String()
}
package codegen

import (
	// "fmt"
	// "github.com/podhmo/goat/internal/metadata"
)

// GenerateMain creates the Go code string for the new main() function
// based on the extracted command metadata.
// This is a placeholder for future implementation.
func GenerateMain(cmdMeta /* *metadata.CommandMetadata */) (string, error) {
	// TODO: Implement the logic to generate Go code for:
	// 1. Flag parsing using the "flag" package, based on cmdMeta.Options
	//    - Set up flags with names, types, help text, default values.
	//    - Handle required flags.
	//    - Handle enum validation.
	//    - Read from environment variables.
	// 2. Call the original run function (cmdMeta.RunFunc.Name) with the populated options struct.
	// 3. Handle errors from the run function.
	// 4. Include necessary imports.

	return "// TODO: Generated main function content will be here.\n", nil
}
package codegen

import (
	// "fmt"
	// "go/ast"
	// "go/token"
	// "os"
)

// WriteMain takes the original file's AST, the new main function content (as string),
// and the position of the old main function, then writes the modified content.
// This is a placeholder for future implementation.
func WriteMain(filePath string, fileAst /* *ast.File */, newMainContent string, mainFuncPos /* *token.Position */) error {
	// TODO: Implement the logic to:
	// 1. Read the original file content.
	// 2. Identify the start and end byte offsets of the existing main() function using fileAst and mainFuncPos.
	//    (This needs careful handling of comments, esp. doc comments of main).
	// 3. Replace the old main() function block with newMainContent.
	//    Alternatively, if mainFuncPos is nil (main not found), append newMainContent.
	// 4. Write the modified content back to filePath or to a new file.
	//
	// Consider using go/format.Source to format the generated code before writing.

	// log.Printf("TODO: Write new main content to %s. Current main position: %v", filePath, mainFuncPos)
	// log.Println("New main content preview:\n", newMainContent)
	return nil
}
package astutils

import (
	"fmt"
	"go/ast"
	"go/token"
	"log"
	"strconv"
	"strings"
)

// ExprToTypeName converts an ast.Expr (representing a type) to its string representation.
func ExprToTypeName(expr ast.Expr) string {
	if expr == nil {
		return ""
	}
	// This is a simplified version. For full accuracy, `go/types` might be needed,
	// but the project aims to avoid it.
	switch t := expr.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.SelectorExpr: // For types like `pkg.Type`
		return fmt.Sprintf("%s.%s", ExprToTypeName(t.X), t.Sel.Name)
	case *ast.StarExpr: // For pointer types like `*Type`
		return "*" + ExprToTypeName(t.X)
	case *ast.ArrayType: // For slice/array types like `[]Type` or `[5]Type`
		// TODO: Differentiate array and slice if necessary
		return "[]" + ExprToTypeName(t.Elt)
	case *ast.MapType:
		return fmt.Sprintf("map[%s]%s", ExprToTypeName(t.Key), ExprToTypeName(t.Value))
	// TODO: Add other types as needed (FuncType, ChanType, InterfaceType, etc.)
	default:
		return fmt.Sprintf("<unsupported_type_expr: %T>", expr)
	}
}

// IsPointerType checks if an ast.Expr represents a pointer type.
func IsPointerType(expr ast.Expr) bool {
	_, ok := expr.(*ast.StarExpr)
	return ok
}

// GetFullFunctionName extracts package alias and function name from a call expression's Fun field.
// Example: for `pkg.MyFunc()`, returns ("MyFunc", "pkg"). For `MyFunc()`, returns ("MyFunc", "").
func GetFullFunctionName(funExpr ast.Expr) (name string, pkgAlias string) {
	switch f := funExpr.(type) {
	case *ast.Ident: // Local function call
		return f.Name, ""
	case *ast.SelectorExpr: // Package function call (e.g. goat.Default)
		if xIdent, ok := f.X.(*ast.Ident); ok {
			return f.Sel.Name, xIdent.Name
		}
	}
	return "", ""
}

// GetImportPath returns the full import path for a given package alias used in the file.
func GetImportPath(fileAst *ast.File, pkgAlias string) string {
	if pkgAlias == "" { // Local function or builtin
		return ""
	}
	for _, importSpec := range fileAst.Imports {
		var aliasName string
		if importSpec.Name != nil { // Import has an explicit alias (e.g., `g "github.com/org/goat"`)
			aliasName = importSpec.Name.Name
		} else { // Import uses default package name (e.g. `import "fmt"`)
			// The actual package name is the last part of the path.
			// This is tricky if the package name differs from the last path element.
			// For simplicity, we assume the alias matches the package name if not explicit.
			// A more robust solution might involve loading package info.
			path := strings.Trim(importSpec.Path.Value, `"`)
			parts := strings.Split(path, "/")
			aliasName = parts[len(parts)-1]
		}

		if aliasName == pkgAlias {
			return strings.Trim(importSpec.Path.Value, `"`)
		}
	}
	return "" // Alias not found in imports
}


// EvaluateArg tries to evaluate an AST expression (typically a function argument)
// to a Go literal value. It supports basic literals.
// Returns the evaluated value or nil if not a simple literal.
func EvaluateArg(arg ast.Expr) any {
	switch v := arg.(type) {
	case *ast.BasicLit:
		switch v.Kind {
		case token.INT:
			i, err := strconv.ParseInt(v.Value, 0, 64)
			if err == nil {
				return i
			}
		case token.FLOAT:
			f, err := strconv.ParseFloat(v.Value, 64)
			if err == nil {
				return f
			}
		case token.STRING:
			s, err := strconv.Unquote(v.Value)
			if err == nil {
				return s
			}
		case token.CHAR:
			s, err := strconv.Unquote(v.Value) // char is rune, often represented as string in Go
			if err == nil && len(s) == 1 {
				return []rune(s)[0]
			}
		}
		log.Printf("EvaluateArg: unhandled basic literal kind %v for value %s", v.Kind, v.Value)
		return v.Value // return raw token value as string
	case *ast.Ident:
		// Could be a predefined constant like `true`, `false`, `nil`
		switch v.Name {
		case "true":
			return true
		case "false":
			return false
		case "nil":
			return nil
		}
		// TODO: Could try to resolve other idents if we had a symbol table
		log.Printf("EvaluateArg: unhandled identifier %s", v.Name)
	// TODO: Add *ast.UnaryExpr for negative numbers (-1)
	// TODO: Add *ast.CompositeLit for simple slice/map literals if needed directly as default
	default:
		log.Printf("EvaluateArg: unhandled expression type %T", arg)
	}
	return nil
}

// EvaluateSliceArg tries to evaluate an AST expression (typically an argument to goat.Enum)
// that should be a slice literal (e.g., []string{"a", "b"}) into a []any.
func EvaluateSliceArg(arg ast.Expr) []any {
	compLit, ok := arg.(*ast.CompositeLit)
	if !ok {
		log.Printf("EvaluateSliceArg: argument is not a composite literal, got %T", arg)
		return nil
	}
	// TODO: Check compLit.Type if necessary (e.g. `[]string{...}`)

	var results []any
	for _, elt := range compLit.Elts {
		val := EvaluateArg(elt) // Each element should be a basic literal
		if val != nil {
			results = append(results, val)
		} else {
			log.Printf("EvaluateSliceArg: could not evaluate element %T in slice", elt)
			// Decide if we should return nil or partial results on error
		}
	}
	return results
}
package stringutils

import (
	"regexp"
	"strings"
)

var (
	matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
	matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")
)

// ToKebabCase converts a string from CamelCase or PascalCase to kebab-case.
// Example: "UserName" -> "user-name", "MinLength" -> "min-length"
func ToKebabCase(str string) string {
	if str == "" {
		return ""
	}
	snake := matchFirstCap.ReplaceAllString(str, "${1}-${2}")
	snake = matchAllCap.ReplaceAllString(snake, "${1}-${2}")
	return strings.ToLower(snake)
}
# goat 🐐

`goat` is a Go command-line tool that works with `go generate` to automate the creation of command-line interfaces from your Go `main.go` (or other specified) files.

## Overview

The core idea is to define a `run(options MyOptions) error` function and an `Options` struct in your Go program. `goat` will then parse this structure, along with special marker functions like `goat.Default()` and `goat.Enum()`, to generate the necessary CLI boilerplate (flag parsing, help messages, environment variable loading, etc.) directly into your `main()` function.

This project is currently in the **early stages of development**.

## Features (Planned & In Progress)

*   **Automatic CLI generation:** Parses `Options` struct fields (name, type, comments, tags) to create CLI flags.
*   **Help message generation:** Creates comprehensive help messages based on comments and option attributes.
*   **Default values:** Supports default values via `goat.Default()` marker function.
*   **Enum validation:** Supports enum-like restricted values via `goat.Enum()` marker function.
*   **Environment variable loading:** Reads option values from environment variables specified in struct tags (e.g., `env:"MY_VAR"`).
*   **Required flags:** Non-pointer fields in the `Options` struct are treated as required.
*   **AST-based:** Operates directly on the Go Abstract Syntax Tree, avoiding reflection at runtime for the generated CLI.
*   **`go generate` integration:** Designed to be invoked via `//go:generate goat ...` comments.

## Tentative Usage

In your `main.go`:

```go
package main

import (
	"fmt"
	"log"

	"github.com/podhmo/goat/goat" // Import goat marker package
)

//go:generate goat -run RunApp -initializer NewAppOptions main.go

// AppOptions defines the command-line options for our application.
// This application serves as a demonstration of goat's capabilities.
type AppOptions struct {
	// Name of the user to greet. This is a very important field.
	UserName string `env:"APP_USER_NAME"`

	// Port for the server to listen on.
	Port int `env:"APP_PORT"`

	// Verbose enables verbose logging. Optional.
	Verbose *bool `env:"APP_VERBOSE"`

	// LogLevel sets the logging level for the application.
	LogLevel string `env:"APP_LOG_LEVEL"`

	// Mode of operation.
	Mode string
}

// NewAppOptions creates a new AppOptions with default values and enum constraints.
func NewAppOptions() *AppOptions {
	return &AppOptions{
		UserName: goat.Default("Guest"),
		Port:     goat.Default(8080),
		LogLevel: goat.Default("info", goat.Enum([]string{"debug", "info", "warn", "error"})),
		Mode:     goat.Enum([]string{"dev", "prod"}),
	}
}

// RunApp is the main application logic.
// It receives configured options and executes the core functionality.
func RunApp(opts AppOptions) error {
	log.Printf("Running app with options: %+v\n", opts)
	fmt.Printf("Hello, %s!\n", opts.UserName)
	if opts.Verbose != nil && *opts.Verbose {
		fmt.Println("Verbose mode enabled.")
	}
	fmt.Printf("Server would run on port: %d\n", opts.Port)
	fmt.Printf("Log level: %s\n", opts.LogLevel)
	fmt.Printf("Mode: %s\n", opts.Mode)
	return nil
}

// main function will be generated/overwritten by goat.
// You can have a simple main for development before generation.
func main() {
	// This content will be replaced by goat.
	// For local development, you might manually call:
	//
	// opts := NewAppOptions()
	// if err := RunApp(*opts); err != nil {
	// 	 log.Fatal(err)
	// }
	log.Println("Original main.go - This will be replaced by goat.")
}

```

Then run:

```bash
go generate
go build -o myapp
./myapp --help
```

This would (ideally) produce a CLI tool with flags derived from `AppOptions`.

## Development

(Details on building `goat` itself, running tests, etc. will go here.)

```bash
# To build the goat tool itself
# cd cmd/goat
# go build -o ../../goat_tool # builds to project root as goat_tool
```

## Contributing

(Contribution guidelines will go here.)

## License

(License information will go here, e.g., MIT License.)
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/podhmo/goat/goat" // Assuming goat markers are in this path
)

//go:generate goat -run RunSimpleApp -initializer NewSimpleOptions main.go
// An alternative could be:
//go:generate ../../goat_tool -run RunSimpleApp -initializer NewSimpleOptions main.go
// (if goat_tool is built to project root)

// SimpleOptions defines the command line options for this simple example tool.
// This tool demonstrates the basic capabilities of goat for CLI generation.
type SimpleOptions struct {
	// Name of the person to greet. This is a mandatory field.
	Name string `env:"SIMPLE_NAME"`

	// Age of the person. This is an optional field.
	Age *int `env:"SIMPLE_AGE"`

	// LogLevel for the application output.
	// It can be one of: debug, info, warning, error.
	LogLevel string `env:"SIMPLE_LOG_LEVEL"`

	// Features to enable, provided as a comma-separated list.
	// Example: --features feat1,feat2
	Features []string `env:"SIMPLE_FEATURES"`

	// OutputDir for any generated files or reports.
	// Defaults to "output" if not specified by the user.
	OutputDir string

	// Mode of operation for the tool, affecting its behavior.
	Mode string `env:"SIMPLE_MODE"`

	// Enable extra verbose output.
	SuperVerbose bool `env:"SIMPLE_SUPER_VERBOSE"`
}

// NewSimpleOptions initializes SimpleOptions with default values and enum constraints.
// This function will be "interpreted" by the goat tool.
func NewSimpleOptions() *SimpleOptions {
	return &SimpleOptions{
		Name:      goat.Default("World"), // Default name
		LogLevel:  goat.Default("info", goat.Enum([]string{"debug", "info", "warning", "error"})),
		OutputDir: goat.Default("output"),
		Mode:      goat.Enum([]string{"standard", "turbo", "eco"}), // Enum without explicit default
		// Age is optional (pointer) and has no default here.
		// Features is a slice, will be handled by flag package (e.g. multiple --features flag or comma sep)
		// SuperVerbose is a bool, defaults to false (zero value for bool)
	}
}

// RunSimpleApp is the core logic for this simple CLI tool.
// It receives the parsed and validated options.
// This function's doc comment is used as the main help text for the command.
func RunSimpleApp(opts SimpleOptions) error {
	fmt.Printf("Hello, %s!\n", opts.Name)

	if opts.Age != nil {
		fmt.Printf("You are %d years old.\n", *opts.Age)
	} else {
		fmt.Println("Your age was not provided.")
	}

	fmt.Printf("Log Level: %s\n", opts.LogLevel)
	fmt.Printf("Output Directory: %s\n", opts.OutputDir)
	fmt.Printf("Mode: %s\n", opts.Mode)

	if len(opts.Features) > 0 {
		fmt.Printf("Enabled features: %v\n", opts.Features)
	} else {
		fmt.Println("No special features enabled.")
	}
	
	if opts.SuperVerbose {
		fmt.Println("Super verbose mode is ON!")
	}

	// Example of returning an error
	if opts.Name == "ErrorTrigger" {
		return fmt.Errorf("the name 'ErrorTrigger' is not allowed")
	}

	return nil
}

// The main function will be overwritten by the `goat` tool.
// For development purposes, you can have a simple main that calls your run function.
func main() {
	log.Println("Original main: This will be replaced by goat.")
	// Example of how you might run it manually during development:
	// opts := NewSimpleOptions()
	// if err := RunSimpleApp(*opts); err != nil {
	// 	fmt.Fprintf(os.Stderr, "Error: %v\n", err)
	// 	os.Exit(1)
	// }
	//
	// Or, to simulate generated main slightly more closely:
	//
	// You would need to parse flags manually here if you want to test that aspect
	// without running `go generate` yet. For simplicity, we just use defaults.
	fmt.Println("Simulating execution with default options before `go generate`:")
	defaultOpts := NewSimpleOptions()
	if err := RunSimpleApp(*defaultOpts); err != nil {
		fmt.Fprintf(os.Stderr, "Application error: %v\n", err)
		os.Exit(1)
	}
}
//go:build ignore
// The line above ensures this file is not part of the normal build.

// This file is solely for demonstrating/triggering `go generate`.
// You would typically place the `//go:generate` directive directly in `main.go`.
// Having it here is just for organizational clarity in this example.

package main

// To run generation for this example, navigate to the `examples/simple` directory
// and run `go generate`.
// You might need to build the `goat` tool first and ensure it's in your PATH
// or provide a relative path to it.
//
// Example if `goat` is in project root as `goat_tool`:
// //go:generate ../../goat_tool -run RunSimpleApp -initializer NewSimpleOptions main.go
//
// If `goat` is in PATH:
// //go:generate goat -run RunSimpleApp -initializer NewSimpleOptions main.go
//
// The directive is usually placed in the `main.go` file itself, like this:
//
// package main
//
// //go:generate goat -run RunSimpleApp -initializer NewSimpleOptions main.go
//
// import ( ... )
// ... rest of main.go

(返答終了理由: STOP)


ユーザー: 実際のところの生成されたコードが省略してしまってる部分はどこですか?

何をどこまで対応してますか?


AIの思考プロセス

Analyzing Omissions in Code

I'm focused on understanding the gaps in the generated code. I'm trying to pinpoint the specific parts that are missing and determine the scope of what has been implemented so far. I'm meticulously examining the output, paying close attention to any areas that seem incomplete or require further development. My goal is a clear picture of what's working and what still needs attention.

Refining the Scope

I'm now focused on the specific omitted and implemented sections, especially within cmd/goat/main.go. I've made significant progress on analyzing AST, notably the run function, Options structure, and their fields, as well as the initialization function interpretation. While code generation and write operations are still commented out, I'm working on help.GenerateHelp, currently focusing on generating a skeleton based on extracted metadata.

Examining Gaps in Functions

I'm focused on the cmd/goat/main.go file, specifically the run function, Options structure, and their fields. I've initiated metadata extraction, including identifying the Default and Enum marker functions. I've successfully implemented skeleton generation within help.GenerateHelp based on the extracted metadata. However, code generation and writing are still pending. I'm also reviewing context.Context parameters and return values in run_func_analyzer.go and evaluating struct tags in options_analyzer.go.

Investigating Function Execution

I'm now diving into the execution flow, specifically within cmd/goat/main.go and the internal/analyzer/ package. The core focus is on the run function, Options structure, and their field analysis. I've initiated metadata extraction, including identification of Default and Enum marker functions. Furthermore, I've successfully implemented skeleton generation for help.GenerateHelp. I'm also scrutinizing context.Context parameters and return values in run_func_analyzer.go and evaluating struct tags in options_analyzer.go, all of which still need further development.

Detailing Current Status

I've been focusing on the critical areas within the generated code, particularly cmd/goat/main.go. I've completed file loading (loader.LoadFile) and made headway on AST analysis (analyzer.Analyze), specifically extracting metadata from the run function, Options structure, and their fields. I'm also working on function interpretation and the identification of marker functions. I've successfully implemented skeleton generation for help.GenerateHelp using collected metadata, although code generation and writing are still paused. I'm also reviewing how to handle context.Context parameters and return values in run_func_analyzer.go, and evaluating struct tags within options_analyzer.go.

Reviewing Progress and Gaps

I'm currently focusing on the areas of the generated code that require further development, particularly in cmd/goat/main.go. I've successfully loaded the file and initiated AST analysis. I'm focusing on metadata extraction for the run function, Options structure, and their fields, as well as function interpretation. The help.GenerateHelp skeleton generation is complete, though code generation remains commented out. I'm also reviewing how context.Context is handled in run_func_analyzer.go and evaluating struct tags in options_analyzer.go, to be incorporated into the current implementation.

Mapping Gaps and Progress

I'm focused on the current status of the code generation, including the gaps and achievements. File loading and initial AST analysis are complete. I'm extracting metadata, and help message generation is also functional. I'm focusing on parsing and interpreting the parameters for the current implementation to assess scope. However, full code generation and writing are still commented out. I'm also reviewing context.Context parameters and return values in run_func_analyzer.go and evaluating struct tags in options_analyzer.go.

Mapping and Addressing Gaps

I've been working on understanding the areas of the code that are still under development, particularly in cmd/goat/main.go. I've successfully loaded the file and performed initial AST analysis to extract metadata, including parsing the run function, Options structure, and associated fields. Help message generation is functional, but code generation and writing operations remain commented out. I'm also reviewing context and struct tag handling, and I am focused on the more critical aspects to be implemented.

AI: はい、先ほど生成したコード群について、どの部分が省略(あるいは未実装)で、現状どこまで対応できているか、ですね。ご説明します。

全体として、「ヘルプメッセージ生成に必要なメタデータの収集と、そのメタデータからのヘルプ文字列生成」という初期目標に向けて、基本的な骨格と主要なロジックの"最初のステップ"が実装されている状態です。コード生成機能(main.go の書き換え)はまだ着手されていません。

主要なコンポーネントごとに、省略箇所と対応状況を説明します。


1. cmd/goat/main.go (CLIツール本体)

  • 省略/未実装箇所:
    • runGoat 関数内のコード生成処理 (codegen.GenerateMain) とファイル書き込み処理 (codegen.WriteMain) の呼び出し部分はコメントアウトされており、実際の処理は行われません。
    • goat ツール自体のより高度なコマンドライン引数(例: main 関数名を指定する -main フラグなど)の完全な実装。
  • 対応状況:
    • goat ツールが受け取る基本的なフラグ (-run, -initializer, 対象ファイル) の解析。
    • 設定 (config.Config) の作成。
    • 主要な処理フロー(ロード → 解析 → 解釈 → ヘルプ生成)の呼び出し。
    • 最終的に生成されたヘルプメッセージの標準出力への表示。

2. internal/loader/loader.go (ファイルロード)

  • 省略/未実装箇所: 特になし。指定されたGoファイルをパースしてASTを構築する基本機能は実装済みです。
  • 対応状況: go/parser を用いてファイルをASTに変換します。コメントもパース対象に含んでいます。

3. internal/analyzer/ (AST解析とメタデータ抽出)

  • analyzer.go (全体統括)
    • 省略/未実装箇所: main 関数の位置を特定するロジック (FindMainFuncPosition) はTODOコメントのみです。これは将来のコード書き換え時に必要になります。
    • 対応状況: AnalyzeRunFuncAnalyzeOptions を呼び出し、主要な CommandMetadataOptions 構造体名を返します。
  • run_func_analyzer.go (run 関数の解析)
    • 省略/未実装箇所:
      • run 関数のパラメータが context.Context を含む場合の、ContextArgNameContextArgType の抽出は行っていますが、その情報を後続処理で活用する部分はまだありません。
      • run 関数の戻り値が厳密に error であるかのチェックはまだありません。
    • 対応状況:
      • 指定された名前の run 関数をASTから探索します。
      • run 関数のドキュメントコメントを抽出して CommandMetadata.Description に設定します。
      • run 関数のパラメータ(1つまたは2つ)から、オプション構造体の引数名と型名を抽出します。
  • options_analyzer.go (Options 構造体の解析)
    • 省略/未実装箇所:
      • 構造体タグで default 値や enum の値を直接指定する機能(例: default:"val", enum:"a,b,c")は未サポートです。これらは現在 goat.Default / goat.Enum マーカー関数経由でのみ設定されます。
      • フィールドの型がネストされた構造体や埋め込み構造体である場合の詳細な解析はまだです。
      • フィールドの型(string, int, bool, []string など)のより詳細な分類と、それに応じたバリデーションルールの生成は将来的な課題です。
    • 対応状況:
      • run 関数の引数型名に基づいて Options 構造体の定義をASTから探索します。
      • エクスポートされたフィールドについて以下を抽出します:
        • フィールド名 (OptionMetadata.Name)
        • CLIフラグ名 (OptionMetadata.CliName、ケバブケースに変換)
        • 型名 (OptionMetadata.TypeName)
        • ポインタ型かどうか (OptionMetadata.IsPointer)、それに基づく必須フラグの基本的な判定 (OptionMetadata.IsRequired)
        • フィールドのドキュメントコメント (OptionMetadata.HelpText)
        • env:"VAR_NAME" 構造体タグからの環境変数名 (OptionMetadata.EnvVar)
  • initializer_analyzer.go: 現状はほぼ空で、特定のロジックはありません。初期化関数の名前は config から渡され、interpreter が直接ASTと関数名を使います。

4. internal/interpreter/interpreter.go (初期化関数の解釈)

  • 省略/未実装箇所:
    • マーカー関数への引数の評価:
      • 現状、goat.Default()goat.Enum() の引数は、リテラル(文字列、数値、[]string{"a", "b"}のようなスライスリテラル)であることしか想定していません。変数が渡された場合の解決や、定数が渡された場合の評価は行いません。
      • goat.Default("default", goat.Enum(myEnumValuesArray)) のように、goat.Enum の結果が別の変数経由で goat.Default に渡されるような、より複雑なパターンの追跡は未対応です。
    • Options 構造体の初期化パターン:
      • 現状は、return &Options{ Field: goat.Default(...) } や、構造体を一度変数に代入してから opts.Field = goat.Default(...) のような直接的な代入パターンを主に想定しています。より間接的な初期化(例: ヘルパー関数を呼び出してその結果をフィールドに設定する)には対応できません。
    • エラーハンドリング: 解釈中のエラーハンドリングはまだ簡素です。
    • evaluator.go にあるべき高度な式評価ロジックはまだ astutils の簡易的なものに依存しており、evaluator.go 自体は実質的な機能を持ちません。
  • 対応状況:
    • 指定された初期化関数(例: NewOptions)のASTを巡回します。
    • return &Options{...} の中のフィールド初期化や、optionsVariable.FieldName = ... という形式の代入文を検出します。
    • 右辺が github.com/podhmo/goat/goat パッケージの Default または Enum 関数の呼び出しであるかを識別します。
      • goat.Default(defaultValue): defaultValue をリテラルとして評価し、OptionMetadata.DefaultValue に設定します。
      • goat.Default(defaultValue, goat.Enum(enumValues)): defaultValueenumValues(スライスリテラル)を評価し、それぞれ OptionMetadata.DefaultValueOptionMetadata.EnumValues に設定します。
      • goat.Enum(enumValues): enumValues を評価し、OptionMetadata.EnumValues に設定します。
    • astutils.GetImportPath を使って、呼び出されている関数が実際に goat パッケージのものであるかを(簡易的に)確認します。

5. internal/help/generator.go (ヘルプメッセージ生成)

  • 省略/未実装箇所:
    • TypeIndicator(ヘルプメッセージ中の型表示)が、より多様なGoの型(map、カスタム型など)に正しく対応していません。現在は基本的な型(string, int, boolなど)とそのスライス程度です。
    • CommandArgsPlaceholder(コマンドが取る引数のプレースホルダー表示)は未対応です。
    • ヘルプメッセージのフォーマットやスタイルは、より洗練させる余地があります(例: オプションのソート順、セクション分けなど)。
  • 対応状況:
    • analyzerinterpreter によって収集・加工された CommandMetadata を受け取ります。
    • text/template を使用して、定義されたフォーマット (helpTemplate) に基づいてヘルプメッセージ文字列を生成します。
    • コマンド名、コマンド説明。
    • 各オプションについて、CLIフラグ名、型インジケータ、ヘルプテキスト、必須情報、デフォルト値、環境変数名、Enumの選択肢を表示します。
    • 複数行のヘルプテキストや説明が適切にインデントされるようになっています。
    • デフォルト値が文字列の場合、クォートで囲むようにしています。

6. internal/codegen/ (コード生成 - main.go 書き換え)

  • 省略/未実装箇所: このパッケージ内の機能はほぼ全て未実装です。
    • main_generator.go: 新しい main() 関数のGoコードを生成するロジックはTODOコメントのみです。具体的には、フラグ定義のコード、環境変数読み込みコード、オプション構造体の初期化、run 関数の呼び出し、エラーハンドリングなどを含むコードを生成する必要があります。
    • writer.go: 生成されたコードで既存の main.gomain() 関数部分を書き換える(またはファイルに追記する)ロジックもTODOコメントのみです。既存関数の範囲特定や安全な書き換えは複雑な処理になります。
  • 対応状況: なし。ファイルと関数のスケルトンのみが存在します。

7. internal/utils/ (ユーティリティ)

  • astutils.go:
    • 省略/未実装箇所:
      • ExprToTypeName: 対応している型が限定的です(map, func, chan, interface などは未対応または不完全)。
      • EvaluateArg, EvaluateSliceArg: 対応しているリテラルの種類が限定的です(例: 負数、より複雑な複合リテラル)。変数の値の解決は行いません。
      • GetImportPath: パッケージのエイリアスと実際のインポートパスのマッピングは、ASTから取得できる範囲での簡易的なものです。
    • 対応状況:
      • ASTノードから型名を文字列として取得する基本的な機能。
      • ノードがポインタ型かどうかを判定。
      • 関数呼び出し式から関数名と(あれば)パッケージエイリアスを取得。
      • パッケージエイリアスからインポートパスを(簡易的に)取得。
      • 基本的なリテラル(int, float, string, char, bool, nil)をGoの値に評価。
      • スライスリテラル(要素が基本リテラルの場合)を []any に評価。
  • stringutils.go:
    • 対応状況: キャメルケース/パスカルケースの文字列をケバブケースに変換する ToKebabCase が実装されています。

8. goat/markers.go (ユーザー向けマーカー関数)

  • 省略/未実装箇所: 特になし。
  • 対応状況: goat.Enum[T](values []T) []Tgoat.Default[T](defaultValue T, enumConstraint ...[]T) T がジェネリクスを用いて定義されており、解析の目印としての役割を果たします。

9. テストコード

  • 全体的にテストコードはまだ記述されていません。

まとめると、現状の goat は、ユーザーが定義した main.go を読み込み、run 関数と Options 構造体、そして NewOptions のような初期化関数内の goat.Default / goat.Enum マーカーを(限定的ながら)解釈して、それらの情報からコマンドラインのヘルプメッセージを生成するところまでが主な機能範囲です。 実際のCLI実行ロジックを main.go に生成・書き込む機能は、これからの実装となります。

(返答終了理由: STOP)


ユーザー: テストコードも生成してください。


AIの思考プロセス

Suggesting Meaningful Tests

I'm now focusing on generating test suggestions for the 'goat' project. My approach is to identify crucial components and propose relevant tests, aiming for meaningful coverage. I'll need to provide templates for these tests to kickstart the process.

Outlining Test Scenarios

I'm now generating test suggestions for the 'goat' project. For internal/loader/loader.go, I plan to test loading valid and invalid Go files. For internal/analyzer/run_func_analyzer.go, I'll check parsing of different run function signatures. Finally, for internal/analyzer/options_analyzer.go, I'll verify the analysis of the Options struct fields, including environment tags.

Defining Test Cases

I've outlined specific test cases for key components in the 'goat' project. For internal/loader/loader.go, I'll check successful and failed file loading. I'll verify that internal/analyzer/run_func_analyzer.go correctly parses different run function signatures. Also, for internal/analyzer/options_analyzer.go, I'll confirm that fields within the Options struct are correctly interpreted. Lastly, I'll examine the interpreter's ability to extract default and enum values, including cases with different combinations.

Generating Comprehensive Tests

I'm now deeply immersed in the process of devising and generating test cases for the 'goat' project. I've broken down each component, like internal/loader/loader.go, internal/analyzer/run_func_analyzer.go, and internal/analyzer/options_analyzer.go. For each component, I'm focusing on crafting tests for valid and invalid file loading, parsing different run signatures, and interpreting the Options struct's fields correctly. My goal is to ensure robust test coverage.

Defining Comprehensive Test Cases

I'm now generating test cases for the 'goat' project. I'll test file loading and error handling for internal/loader/loader.go. For internal/analyzer/run_func_analyzer.go, I'll verify parsing of valid and invalid run function signatures. For internal/analyzer/options_analyzer.go, I'll cover Options struct field interpretation. internal/interpreter/interpreter.go will be tested for default and enum value extraction. Lastly, I'll confirm expected help message generation in internal/help/generator.go.

Generating Test Templates

I'm now focused on generating test templates for the 'goat' project. I plan to use table-driven tests for conciseness. For internal/loader/loader.go, I'll test file loading. For internal/analyzer/run_func_analyzer.go, I'll verify run function signature parsing. I'll also test the analysis of Options fields within internal/analyzer/options_analyzer.go. Further, I'll provide templates for checking goat.Default() and goat.Enum().

Defining Comprehensive Cases

I'm currently focused on defining comprehensive test cases for the 'goat' project. I plan to generate tests for loading valid and invalid Go files in internal/loader/loader.go. For internal/analyzer, I'll test parsing signatures and interpreting struct fields, while also ensuring correct enum and default value extraction in internal/interpreter/interpreter.go. I'll also generate a test for help messages. I will include detailed test case snippets for each component.

Devising Comprehensive Tests

I'm now generating test cases for the 'goat' project. I'll create _test.go files for each package, employing table-driven tests. I'll test file loading and error handling for internal/loader/loader.go. For internal/analyzer/run_func_analyzer.go, I'll verify run function signature parsing, and for internal/analyzer/options_analyzer.go, I'll test Options struct field interpretation. My work includes test case snippets, while aiming for a focused and concise approach for now.

AI: はい、承知いたしました。前回生成した `goat` プロジェクトの主要なコンポーネントに対するテストコードの雛形を生成します。

テストはGoの標準 testing パッケージを使用し、基本的なテストケースのパターンを示すことを目的とします。

package loader

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestLoadFile_Success(t *testing.T) {
	// Create a temporary Go file for testing
	tmpDir := t.TempDir()
	tmpFile := filepath.Join(tmpDir, "test.go")
	content := `
package main

import "fmt"

func main() {
	fmt.Println("Hello")
}
`
	if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
		t.Fatalf("Failed to write temp file: %v", err)
	}

	fileAST, err := LoadFile(tmpFile)
	if err != nil {
		t.Fatalf("LoadFile failed: %v", err)
	}
	if fileAST == nil {
		t.Fatal("LoadFile returned nil AST")
	}
	if fileAST.Name.Name != "main" {
		t.Errorf("Expected package name 'main', got '%s'", fileAST.Name.Name)
	}
}

func TestLoadFile_NonExistentFile(t *testing.T) {
	_, err := LoadFile("non_existent_file.go")
	if err == nil {
		t.Fatal("LoadFile should have failed for a non-existent file, but it did not")
	}
	// Check if the error message is somewhat informative, though exact message depends on os
	if !strings.Contains(err.Error(), "no such file or directory") && !strings.Contains(err.Error(), "cannot find the file") {
		t.Logf("Warning: Error message might not be as expected: %v", err)
	}
}

func TestLoadFile_InvalidGoSyntax(t *testing.T) {
	tmpDir := t.TempDir()
	tmpFile := filepath.Join(tmpDir, "invalid.go")
	content := `
package main
func main() { fmt.Println("Hello" // Missing closing parenthesis
`
	if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
		t.Fatalf("Failed to write temp file: %v", err)
	}

	_, err := LoadFile(tmpFile)
	if err == nil {
		t.Fatal("LoadFile should have failed for a file with syntax errors, but it did not")
	}
	// Error message from parser usually contains line number and expected token
	if !strings.Contains(err.Error(), "expected ')'") && !strings.Contains(err.Error(), "expected declaration") { // depends on parser error detail
		t.Logf("Warning: Syntax error message might not be as expected: %v", err)
	}
}
package analyzer

import (
	"go/parser"
	"go/token"
	"reflect"
	"strings"
	"testing"

	"github.com/podhmo/goat/internal/metadata"
)

func parseTestFile(t *testing.T, content string) *ast.File {
	t.Helper()
	fset := token.NewFileSet()
	fileAst, err := parser.ParseFile(fset, "test.go", content, parser.ParseComments)
	if err != nil {
		t.Fatalf("Failed to parse test file content: %v", err)
	}
	return fileAst
}

func TestAnalyzeRunFunc_Simple(t *testing.T) {
	content := `
package main

// MyRun is the main logic.
// It does important things.
func MyRun(opts Options) error { return nil }
`
	fileAst := parseTestFile(t, content)
	runFuncInfo, doc, err := AnalyzeRunFunc(fileAst, "MyRun")

	if err != nil {
		t.Fatalf("AnalyzeRunFunc failed: %v", err)
	}
	if runFuncInfo == nil {
		t.Fatal("runFuncInfo is nil")
	}

	expectedDoc := "MyRun is the main logic.\nIt does important things."
	if doc != expectedDoc {
		t.Errorf("Expected doc comment '%s', got '%s'", expectedDoc, doc)
	}
	if runFuncInfo.Name != "MyRun" {
		t.Errorf("Expected func name 'MyRun', got '%s'", runFuncInfo.Name)
	}
	if runFuncInfo.OptionsArgName != "opts" {
		t.Errorf("Expected options arg name 'opts', got '%s'", runFuncInfo.OptionsArgName)
	}
	if runFuncInfo.OptionsArgType != "Options" {
		t.Errorf("Expected options arg type 'Options', got '%s'", runFuncInfo.OptionsArgType)
	}
}

func TestAnalyzeRunFunc_WithContext(t *testing.T) {
	content := `
package main

import "context"

// RunWithCtx executes with context.
func RunWithCtx(ctx context.Context, appOpts AppOptions) error { return nil }
`
	fileAst := parseTestFile(t, content)
	runFuncInfo, doc, err := AnalyzeRunFunc(fileAst, "RunWithCtx")

	if err != nil {
		t.Fatalf("AnalyzeRunFunc failed: %v", err)
	}
	if runFuncInfo == nil {
		t.Fatal("runFuncInfo is nil")
	}

	expectedDoc := "RunWithCtx executes with context."
	if doc != expectedDoc {
		t.Errorf("Expected doc comment '%s', got '%s'", expectedDoc, doc)
	}
	if runFuncInfo.Name != "RunWithCtx" {
		t.Errorf("Expected func name 'RunWithCtx', got '%s'", runFuncInfo.Name)
	}
	if runFuncInfo.ContextArgName != "ctx" {
		t.Errorf("Expected context arg name 'ctx', got '%s'", runFuncInfo.ContextArgName)
	}
	if runFuncInfo.ContextArgType != "context.Context" {
		t.Errorf("Expected context arg type 'context.Context', got '%s'", runFuncInfo.ContextArgType)
	}
	if runFuncInfo.OptionsArgName != "appOpts" {
		t.Errorf("Expected options arg name 'appOpts', got '%s'", runFuncInfo.OptionsArgName)
	}
	if runFuncInfo.OptionsArgType != "AppOptions" {
		t.Errorf("Expected options arg type 'AppOptions', got '%s'", runFuncInfo.OptionsArgType)
	}
}

func TestAnalyzeRunFunc_NotFound(t *testing.T) {
	content := `package main; func SomeOtherFunc() {}`
	fileAst := parseTestFile(t, content)
	_, _, err := AnalyzeRunFunc(fileAst, "NonExistentRun")
	if err == nil {
		t.Fatal("AnalyzeRunFunc should have failed for a non-existent function")
	}
	if !strings.Contains(err.Error(), "NonExistentRun' not found") {
		t.Errorf("Unexpected error message: %v", err)
	}
}

func TestAnalyzeRunFunc_InvalidSignature(t *testing.T) {
	content := `package main; func MyRun() error { return nil }` // No params
	fileAst := parseTestFile(t, content)
	_, _, err := AnalyzeRunFunc(fileAst, "MyRun")
	if err == nil {
		t.Fatal("AnalyzeRunFunc should have failed for invalid signature")
	}
	if !strings.Contains(err.Error(), "unexpected signature") {
		t.Errorf("Unexpected error message for invalid signature: %v", err)
	}
}
package analyzer

import (
	// "go/parser" // Already imported in run_func_analyzer_test.go, helper can be shared
	// "go/token"
	"reflect"
	"testing"

	"github.com/podhmo/goat/internal/metadata"
	// "github.com/podhmo/goat/internal/utils/astutils" // For parseTestFile helper if moved
)

func TestAnalyzeOptions_Simple(t *testing.T) {
	content := `
package main

// Config holds configuration.
type Config struct {
	// Name of the user.
	Name string %s
	// Age of the user, optional.
	Age *int %s
	// IsAdmin flag.
	IsAdmin bool %s
	// Features list.
	Features []string %s
}
`
	// Test with and without tags to ensure tags are parsed if present
	testCases := []struct {
		nameTag  string
		ageTag   string
		adminTag string
		featTag  string
		expectedOptions []*metadata.OptionMetadata
	}{
		{
			nameTag:  ` %s`, ageTag: ` %s`, adminTag: ` %s`, featTag: ` %s`,
			expectedOptions: []*metadata.OptionMetadata{
				{Name: "Name", CliName: "name", TypeName: "string", HelpText: "Name of the user.", IsRequired: true},
				{Name: "Age", CliName: "age", TypeName: "*int", HelpText: "Age of the user, optional.", IsPointer: true, IsRequired: false},
				{Name: "IsAdmin", CliName: "is-admin", TypeName: "bool", HelpText: "IsAdmin flag.", IsRequired: true},
				{Name: "Features", CliName: "features", TypeName: "[]string", HelpText: "Features list.", IsRequired: true},
			},
		},
		{
			nameTag:  ` %s`, ageTag: ` %s`, adminTag: ` %s`, featTag: ` %s`,
			expectedOptions: []*metadata.OptionMetadata{
				{Name: "Name", CliName: "name", TypeName: "string", HelpText: "Name of the user.", IsRequired: true, EnvVar: "APP_NAME"},
				{Name: "Age", CliName: "age", TypeName: "*int", HelpText: "Age of the user, optional.", IsPointer: true, IsRequired: false, EnvVar: "USER_AGE"},
				{Name: "IsAdmin", CliName: "is-admin", TypeName: "bool", HelpText: "IsAdmin flag.", IsRequired: true}, // No env tag
				{Name: "Features", CliName: "features", TypeName: "[]string", HelpText: "Features list.", IsRequired: true, EnvVar: "APP_FEATURES"},
			},
		},
	}
	
	// Inject tags into content format string
	testCases[0].nameTag = ""
	testCases[0].ageTag = ""
	testCases[0].adminTag = ""
	testCases[0].featTag = ""

	testCases[1].nameTag = "`env:\"APP_NAME\"`"
	testCases[1].ageTag = "`env:\"USER_AGE\"`"
	testCases[1].adminTag = ""
	testCases[1].featTag = "`env:\"APP_FEATURES\"`"


	for i, tc := range testCases {
		formattedContent := Sprintf(content, tc.nameTag, tc.ageTag, tc.adminTag, tc.featTag)
		fileAst := parseTestFile(t, formattedContent) // Assuming parseTestFile is available
		
		options, structName, err := AnalyzeOptions(fileAst, "Config", "main")
		if err != nil {
			t.Fatalf("Test case %d: AnalyzeOptions failed: %v", i, err)
		}
		if structName != "Config" {
			t.Errorf("Test case %d: Expected struct name 'Config', got '%s'", i, structName)
		}

		if len(options) != len(tc.expectedOptions) {
			t.Fatalf("Test case %d: Expected %d options, got %d. Options: %+v", i, len(tc.expectedOptions), len(options), options)
		}

		for j, opt := range options {
			expectedOpt := tc.expectedOptions[j]
			// Compare relevant fields, reflect.DeepEqual might be too strict for uninitialized fields
			if opt.Name != expectedOpt.Name || opt.CliName != expectedOpt.CliName ||
				opt.TypeName != expectedOpt.TypeName || strings.TrimSpace(opt.HelpText) != strings.TrimSpace(expectedOpt.HelpText) ||
				opt.IsPointer != expectedOpt.IsPointer || opt.IsRequired != expectedOpt.IsRequired ||
				opt.EnvVar != expectedOpt.EnvVar {
				t.Errorf("Test case %d, Option %d: Mismatch.\nExpected: %+v\nGot:      %+v", i, j, expectedOpt, opt)
			}
		}
	}
}

func TestAnalyzeOptions_UnexportedFields(t *testing.T) {
	content := `
package main
type Config struct {
	Exported   string
	unexported string // Should be ignored
}
`
	fileAst := parseTestFile(t, content)
	options, _, err := AnalyzeOptions(fileAst, "Config", "main")
	if err != nil {
		t.Fatalf("AnalyzeOptions failed: %v", err)
	}
	if len(options) != 1 {
		t.Fatalf("Expected 1 option, got %d. Unexported field was not ignored.", len(options))
	}
	if options[0].Name != "Exported" {
		t.Errorf("Expected option name 'Exported', got '%s'", options[0].Name)
	}
}

func TestAnalyzeOptions_StructNotFound(t *testing.T) {
	content := `package main; type OtherStruct struct{}`
	fileAst := parseTestFile(t, content)
	_, _, err := AnalyzeOptions(fileAst, "NonExistentConfig", "main")
	if err == nil {
		t.Fatal("AnalyzeOptions should have failed for a non-existent struct")
	}
	if !strings.Contains(err.Error(), "NonExistentConfig' not found") {
		t.Errorf("Unexpected error message: %v", err)
	}
}
package interpreter

import (
	"go/parser"
	"go/token"
	"reflect"
	"strings"
	"testing"

	"github.com/podhmo/goat/internal/metadata"
)

func parseTestFileForInterpreter(t *testing.T, content string) *ast.File {
	t.Helper()
	fset := token.NewFileSet()
	// Ensure comments are parsed if markers.go uses them, though not typical for func calls
	fileAst, err := parser.ParseFile(fset, "test.go", content, parser.ParseComments)
	if err != nil {
		t.Fatalf("Failed to parse test file content: %v", err)
	}
	return fileAst
}

const goatPkgImportPath = "github.com/podhmo/goat/goat"

func TestInterpretInitializer_SimpleDefaults(t *testing.T) {
	content := `
package main
import g "github.com/podhmo/goat/goat"

type Options struct {
	Name string
	Port int
	Verbose bool
}

func NewOpts() *Options {
	return &Options{
		Name: g.Default("guest"),
		Port: g.Default(8080),
		Verbose: g.Default(true),
	}
}
`
	fileAst := parseTestFileForInterpreter(t, content)
	optionsMeta := []*metadata.OptionMetadata{
		{Name: "Name", CliName: "name", TypeName: "string"},
		{Name: "Port", CliName: "port", TypeName: "int"},
		{Name: "Verbose", CliName: "verbose", TypeName: "bool"},
	}

	err := InterpretInitializer(fileAst, "Options", "NewOpts", optionsMeta, goatPkgImportPath)
	if err != nil {
		t.Fatalf("InterpretInitializer failed: %v", err)
	}

	expectedDefaults := map[string]any{
		"Name":    "guest",
		"Port":    int64(8080), // parser reads numbers as int64 initially
		"Verbose": true,
	}

	for _, opt := range optionsMeta {
		expected, ok := expectedDefaults[opt.Name]
		if !ok {
			t.Errorf("Unexpected option %s found in results", opt.Name)
			continue
		}
		if !reflect.DeepEqual(opt.DefaultValue, expected) {
			t.Errorf("For option %s, expected default %v (type %T), got %v (type %T)",
				opt.Name, expected, expected, opt.DefaultValue, opt.DefaultValue)
		}
	}
}

func TestInterpretInitializer_EnumAndCombined(t *testing.T) {
	content := `
package main
import "github.com/podhmo/goat/goat" // Direct import

type Options struct {
	Level string
	Mode  string
}

func InitOptions() *Options {
	return &Options{
		Level: goat.Default("info", goat.Enum([]string{"debug", "info", "warn", "error"})),
		Mode:  goat.Enum([]string{"fast", "slow"}),
	}
}
`
	fileAst := parseTestFileForInterpreter(t, content)
	optionsMeta := []*metadata.OptionMetadata{
		{Name: "Level", CliName: "level", TypeName: "string"},
		{Name: "Mode", CliName: "mode", TypeName: "string"},
	}

	err := InterpretInitializer(fileAst, "Options", "InitOptions", optionsMeta, goatPkgImportPath)
	if err != nil {
		t.Fatalf("InterpretInitializer failed: %v", err)
	}

	// Check Level
	levelOpt := optionsMeta[0]
	if levelOpt.DefaultValue != "info" {
		t.Errorf("Level: Expected default 'info', got '%v'", levelOpt.DefaultValue)
	}
	expectedLevelEnum := []any{"debug", "info", "warn", "error"}
	if !reflect.DeepEqual(levelOpt.EnumValues, expectedLevelEnum) {
		t.Errorf("Level: Expected enum %v, got %v", expectedLevelEnum, levelOpt.EnumValues)
	}

	// Check Mode
	modeOpt := optionsMeta[1]
	if modeOpt.DefaultValue != nil { // Mode only has Enum, no Default value explicitly set by goat.Default
		t.Errorf("Mode: Expected no default value, got '%v'", modeOpt.DefaultValue)
	}
	expectedModeEnum := []any{"fast", "slow"}
	if !reflect.DeepEqual(modeOpt.EnumValues, expectedModeEnum) {
		t.Errorf("Mode: Expected enum %v, got %v", expectedModeEnum, modeOpt.EnumValues)
	}
}

func TestInterpretInitializer_AssignmentStyle(t *testing.T) {
	content := `
package main
import customgoat "github.com/podhmo/goat/goat"

type Options struct {
	Path string
}

func New() *Options {
	opts := &Options{}
	opts.Path = customgoat.Default("/tmp")
	return opts
}
`
	fileAst := parseTestFileForInterpreter(t, content)
	optionsMeta := []*metadata.OptionMetadata{
		{Name: "Path", CliName: "path", TypeName: "string"},
	}

	err := InterpretInitializer(fileAst, "Options", "New", optionsMeta, goatPkgImportPath)
	if err != nil {
		t.Fatalf("InterpretInitializer with assignment style failed: %v", err)
	}

	if optionsMeta[0].DefaultValue != "/tmp" {
		t.Errorf("Path: Expected default '/tmp', got '%v'", optionsMeta[0].DefaultValue)
	}
}


func TestInterpretInitializer_NonGoatPackageCall(t *testing.T) {
	content := `
package main
import g "github.com/some/other/pkg" // Different package

type Options struct { Name string }
func New() *Options {
	// This call should be ignored by the interpreter if markerPkgImportPath is specific
	return &Options{ Name: g.Default("ignored") }
}
`
	fileAst := parseTestFileForInterpreter(t, content)
	optionsMeta := []*metadata.OptionMetadata{{Name: "Name"}}

	err := InterpretInitializer(fileAst, "Options", "New", optionsMeta, goatPkgImportPath) // goatPkgImportPath is for "github.com/podhmo/goat/goat"
	if err != nil {
		t.Fatalf("InterpretInitializer failed: %v", err)
	}
	if optionsMeta[0].DefaultValue != nil {
		t.Errorf("Expected DefaultValue to be nil as g.Default is not from goat package, got %v", optionsMeta[0].DefaultValue)
	}
}

func TestInterpretInitializer_InitializerNotFound(t *testing.T) {
	content := `package main; type Options struct{}`
	fileAst := parseTestFileForInterpreter(t, content)
	err := InterpretInitializer(fileAst, "Options", "NonExistentInit", nil, goatPkgImportPath)
	if err == nil {
		t.Fatal("InterpretInitializer should fail if initializer func not found")
	}
	if !strings.Contains(err.Error(), "NonExistentInit' not found") {
		t.Errorf("Unexpected error message: %v", err)
	}
}
package help

import (
	"strings"
	"testing"

	"github.com/podhmo/goat/internal/metadata"
)

func TestGenerateHelp_Basic(t *testing.T) {
	cmdMeta := &metadata.CommandMetadata{
		Name:        "mytool",
		Description: "A super useful tool.\nDoes amazing things.",
		RunFunc: &metadata.RunFuncInfo{
			Name: "Run",
		},
		Options: []*metadata.OptionMetadata{
			{
				Name:       "Username",
				CliName:    "username",
				TypeName:   "string",
				HelpText:   "The username for login.",
				IsRequired: true,
				EnvVar:     "APP_USER",
			},
			{
				Name:         "Port",
				CliName:      "port",
				TypeName:     "int",
				HelpText:     "Port number to listen on.",
				IsRequired:   false,
				DefaultValue: 8080,
			},
			{
				Name:       "Mode",
				CliName:    "mode",
				TypeName:   "string",
				HelpText:   "Operation mode.",
				DefaultValue: "dev",
				EnumValues: []any{"dev", "prod", "test"},
			},
			{
				Name:       "Verbose",
				CliName:    "verbose",
				TypeName:   "*bool", // Pointer bool
				HelpText:   "Enable verbose output.",
				IsPointer:  true,
				IsRequired: false,
			},
		},
	}

	helpMsg := GenerateHelp(cmdMeta)

	// Check for command name and description
	if !strings.Contains(helpMsg, "mytool - A super useful tool.") {
		t.Errorf("Help message missing or incorrect command title. Got:\n%s", helpMsg)
	}
	if !strings.Contains(helpMsg, "\n         Does amazing things.") { // Check multiline indent for desc
		t.Errorf("Help message missing or incorrect multiline description. Got:\n%s", helpMsg)
	}

	// Check for username option
	if !strings.Contains(helpMsg, "--username string") {
		t.Errorf("Missing --username string. Got:\n%s", helpMsg)
	}
	if !strings.Contains(helpMsg, "The username for login. (required) (env: APP_USER)") {
		t.Errorf("Missing or incorrect help for username. Got:\n%s", helpMsg)
	}

	// Check for port option
	if !strings.Contains(helpMsg, "--port int") {
		t.Errorf("Missing --port int. Got:\n%s", helpMsg)
	}
	if !strings.Contains(helpMsg, "Port number to listen on. (default: 8080)") {
		t.Errorf("Missing or incorrect help for port. Got:\n%s", helpMsg)
	}
	
	// Check for mode option
	if !strings.Contains(helpMsg, "--mode string") {
		t.Errorf("Missing --mode string. Got:\n%s", helpMsg)
	}
	if !strings.Contains(helpMsg, "Operation mode. (default: \"dev\") (allowed: dev, prod, test)") {
		t.Errorf("Missing or incorrect help for mode. Got:\n%s", helpMsg)
	}
	
	// Check for verbose option
	if !strings.Contains(helpMsg, "--verbose bool") { // Type indicator becomes "bool"
		t.Errorf("Missing --verbose bool. Got:\n%s", helpMsg)
	}
	if !strings.Contains(helpMsg, "Enable verbose output.") { // No (required), (default)
		t.Errorf("Missing or incorrect help for verbose. Got:\n%s", helpMsg)
	}
	if strings.Contains(helpMsg, "--verbose bool (required)") || strings.Contains(helpMsg, "--verbose bool (default:") {
		t.Errorf("Verbose option should not be marked as required or have a default in help text. Got:\n%s", helpMsg)
	}


	// Check for standard help flag
	if !strings.Contains(helpMsg, "-h, --help             Show this help message and exit") {
		t.Errorf("Standard help flag -h, --help is missing. Got:\n%s", helpMsg)
	}

	// t.Log(helpMsg) // For manual inspection if needed
}

func TestGenerateHelp_NilMetadata(t *testing.T) {
	helpMsg := GenerateHelp(nil)
	if !strings.Contains(helpMsg, "Error: Command metadata is nil.") {
		t.Errorf("Expected error message for nil metadata, got: %s", helpMsg)
	}
}
package astutils

import (
	"go/parser"
	"go/token"
	"reflect"
	"testing"
)

func parseAndFindFirstFuncArgType(t *testing.T, code string, funcName string) ast.Expr {
	t.Helper()
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "test.go", code, 0)
	if err != nil {
		t.Fatalf("Failed to parse code: %v", err)
	}
	var targetExpr ast.Expr
	ast.Inspect(f, func(n ast.Node) bool {
		if fn, ok := n.(*ast.FuncDecl); ok && fn.Name.Name == funcName {
			if fn.Type.Params != nil && len(fn.Type.Params.List) > 0 {
				targetExpr = fn.Type.Params.List[0].Type
				return false
			}
		}
		return true
	})
	if targetExpr == nil {
		t.Fatalf("Could not find func %s or its first argument type", funcName)
	}
	return targetExpr
}

func TestExprToTypeName(t *testing.T) {
	testCases := []struct {
		name     string
		code     string
		funcName string
		expected string
	}{
		{"Ident", `package main; type MyType string; func T(a MyType){}`, "T", "MyType"},
		{"StarExpr", `package main; type MyType string; func T(a *MyType){}`, "T", "*MyType"},
		{"SelectorExpr", `package main; import "io"; func T(a io.Reader){}`, "T", "io.Reader"},
		{"ArrayTypeSlice", `package main; type MyType string; func T(a []MyType){}`, "T", "[]MyType"},
		{"ArrayTypePointerSlice", `package main; type MyType string; func T(a []*MyType){}`, "T", "[]*MyType"},
		{"MapType", `package main; func T(a map[string]int){}`, "T", "map[string]int"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			expr := parseAndFindFirstFuncArgType(t, tc.code, tc.funcName)
			actual := ExprToTypeName(expr)
			if actual != tc.expected {
				t.Errorf("Expected type name '%s', got '%s'", tc.expected, actual)
			}
		})
	}
}

func TestIsPointerType(t *testing.T) {
	codeIsPtr := `package main; type MyType int; func PtrFunc(a *MyType){}`
	codeIsNotPtr := `package main; type MyType int; func NonPtrFunc(a MyType){}`

	exprIsPtr := parseAndFindFirstFuncArgType(t, codeIsPtr, "PtrFunc")
	if !IsPointerType(exprIsPtr) {
		t.Error("Expected IsPointerType to be true for *MyType")
	}

	exprIsNotPtr := parseAndFindFirstFuncArgType(t, codeIsNotPtr, "NonPtrFunc")
	if IsPointerType(exprIsNotPtr) {
		t.Error("Expected IsPointerType to be false for MyType")
	}
}


func parseAndFindFirstCallExprFun(t *testing.T, code string, targetVar string) ast.Expr {
    t.Helper()
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "test.go", code, 0)
    if err != nil {
        t.Fatalf("Failed to parse code: %v", err)
    }
    var targetExpr ast.Expr
    ast.Inspect(f, func(n ast.Node) bool {
        if assign, ok := n.(*ast.AssignStmt); ok {
            if len(assign.Lhs) == 1 && len(assign.Rhs) == 1 {
                if ident, ok := assign.Lhs[0].(*ast.Ident); ok && ident.Name == targetVar {
                    if call, ok := assign.Rhs[0].(*ast.CallExpr); ok {
                        targetExpr = call.Fun
                        return false
                    }
                }
            }
        }
        return true
    })
    if targetExpr == nil {
        t.Fatalf("Could not find call expression assigned to %s", targetVar)
    }
    return targetExpr
}


func TestGetFullFunctionName(t *testing.T) {
    testCases := []struct {
        name         string
        code         string
        varToInspect string // Variable whose assigned CallExpr.Fun we inspect
        expectedName string
        expectedPkg  string
    }{
        {"LocalFunc", `package main; func local() {}; func T() { x := local() }`, "x", "local", ""},
        {"PkgFunc", `package main; import p "pkg.com/lib"; func T() { y := p.Remote() }`, "y", "Remote", "p"},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            callFunExpr := parseAndFindFirstCallExprFun(t, tc.code, tc.varToInspect)
            actualName, actualPkg := GetFullFunctionName(callFunExpr)
            if actualName != tc.expectedName || actualPkg != tc.expectedPkg {
                t.Errorf("Expected (%s, %s), got (%s, %s)", tc.expectedName, tc.expectedPkg, actualName, actualPkg)
            }
        })
    }
}


func parseFileForImports(t *testing.T, code string) *ast.File {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "testimports.go", code, parser.ImportsOnly)
	if err != nil {
		t.Fatalf("Failed to parse imports: %v", err)
	}
	return f
}

func TestGetImportPath(t *testing.T) {
	code := `
package main

import (
	"fmt"
	myio "io"
	"os"
	_ "github.com/lib/pq" // blank import
	. "github.com/onsi/ginkgo" // dot import
	custom_path "github.com/custom/module/v2"
)
`
	fileAst := parseFileForImports(t, code)
	testCases := []struct {
		alias    string
		expected string
	}{
		{"fmt", "fmt"},
		{"myio", "io"},
		{"os", "os"},
		{"pq", "github.com/lib/pq"}, // Assumes alias matches last part if Name is nil
		{"ginkgo", "github.com/onsi/ginkgo"}, // for dot import, alias is package name
		{"custom_path", "github.com/custom/module/v2"},
		{"nonexistent", ""},
		{"", ""}, // local or builtin
	}

	for _, tc := range testCases {
		t.Run(tc.alias, func(t *testing.T) {
			actual := GetImportPath(fileAst, tc.alias)
			if actual != tc.expected {
				t.Errorf("For alias '%s', expected import path '%s', got '%s'", tc.alias, tc.expected, actual)
			}
		})
	}
}

func parseExpr(t *testing.T, exprStr string) ast.Expr {
	expr, err := parser.ParseExpr(exprStr)
	if err != nil {
		t.Fatalf("Failed to parse expr '%s': %v", exprStr, err)
	}
	return expr
}

func TestEvaluateArg(t *testing.T) {
	testCases := []struct {
		name     string
		exprStr  string
		expected any
	}{
		{"Int", "123", int64(123)},
		{"String", `"hello"`, "hello"},
		{"Float", "123.45", 123.45},
		{"True", "true", true},
		{"False", "false", false},
		{"Nil", "nil", nil},
		// TODO: Add char, negative numbers, etc.
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			expr := parseExpr(t, tc.exprStr)
			actual := EvaluateArg(expr)
			if !reflect.DeepEqual(actual, tc.expected) {
				t.Errorf("For expr '%s', expected %v (type %T), got %v (type %T)",
					tc.exprStr, tc.expected, tc.expected, actual, actual)
			}
		})
	}
}

func TestEvaluateSliceArg(t *testing.T) {
	testCases := []struct {
		name     string
		exprStr  string
		expected []any
	}{
		{"StringSlice", `[]string{"a", "b", "c"}`, []any{"a", "b", "c"}},
		{"IntSlice", `[]int{1, 2, 3}`, []any{int64(1), int64(2), int64(3)}},
		{"MixedSliceNotDirectlySupportedByBasicLit", `[]any{"a", 1}`, nil}, // EvaluateArg handles elements individually
		{"EmptySlice", `[]string{}`, []any{}},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			exprNode, err := parser.ParseExpr(tc.exprStr) // Use ParseExpr for slice literals
			if err != nil {
				t.Fatalf("Failed to parse expr %s: %v", tc.exprStr, err)
			}
			actual := EvaluateSliceArg(exprNode)
			if tc.name == "MixedSliceNotDirectlySupportedByBasicLit" { // special case for current limitations
				if len(actual) != 2 || actual[0] != "a" || actual[1] != int64(1) {
					t.Errorf("For expr '%s', expected evaluated elements, got %v", tc.exprStr, actual)
				}
				return
			}

			if !reflect.DeepEqual(actual, tc.expected) {
				t.Errorf("For expr '%s', expected %v, got %v", tc.exprStr, tc.expected, actual)
			}
		})
	}
}
package stringutils

import "testing"

func TestToKebabCase(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  string
	}{
		{"empty", "", ""},
		{"lowercase", "test", "test"},
		{"camelCase", "testString", "test-string"},
		{"PascalCase", "TestString", "test-string"},
		{"withNumber", "testString123", "test-string123"},
		{"numberInMiddle", "test123String", "test123-string"},
		{"allCaps", "TEST", "test"},
		{"mixedCaps", "TestHTTPResponse", "test-http-response"},
		{"singleWordCaps", "URL", "url"},
		{"leadingCaps", "HTTPRequest", "http-request"},
		{"snake_case_input", "test_string", "test_string"}, // No change for snake_case
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ToKebabCase(tt.input); got != tt.want {
				t.Errorf("ToKebabCase(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}
package goat

import (
	"reflect"
	"testing"
)

// Test for Enum marker function
func TestEnum(t *testing.T) {
	testCases := []struct {
		name     string
		input    []any // Using []any for broader testability, though T is specific at call site
		expected []any
	}{
		{
			name:     "string slice",
			input:    []any{"alpha", "beta", "gamma"},
			expected: []any{"alpha", "beta", "gamma"},
		},
		{
			name:     "int slice",
			input:    []any{1, 2, 3},
			expected: []any{1, 2, 3},
		},
		{
			name:     "empty slice",
			input:    []any{},
			expected: []any{},
		},
		{
			name:     "nil slice",
			input:    nil,
			expected: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var result []any // Adapt based on type of tc.input if needed, or keep generic
			switch v := tc.input.(type) {
			case []any: // This path matches our test case structure
				// To call the generic Enum, we need a typed slice.
				// This test primarily checks if Enum returns its input, type conversion is for test setup.
				if len(v) > 0 {
					switch v[0].(type) {
					case string:
						typedInput := make([]string, len(v))
						for i, item := range v { typedInput[i] = item.(string) }
						resultTyped := Enum(typedInput)
						result = make([]any, len(resultTyped))
						for i, item := range resultTyped { result[i] = item }
					case int:
						typedInput := make([]int, len(v))
						for i, item := range v { typedInput[i] = item.(int) }
						resultTyped := Enum(typedInput)
						result = make([]any, len(resultTyped))
						for i, item := range resultTyped { result[i] = item }
					default:
						if v == nil || len(v) == 0 { // Handle empty or nil explicitly
							result = Enum(v) // Call with []any if it works or specific typed nil
						} else {
							t.Skipf("Test setup for type %T not fully implemented for Enum test", v[0])
						}
					}
				} else { // empty or nil slice
					if v == nil {
						result = Enum[any](nil) // Explicitly type for nil
					} else {
						result = Enum(v) // for empty []any{}
					}
				}


			default:
				if tc.input == nil {
					result = Enum[any](nil) // Test with nil explicitly typed
				} else {
					t.Fatalf("Unsupported input type for Enum test: %T", tc.input)
				}
			}


			if !reflect.DeepEqual(result, tc.expected) {
				t.Errorf("Enum(%v) = %v, want %v", tc.input, result, tc.expected)
			}
		})
	}
}

// Test for Default marker function
func TestDefault(t *testing.T) {
	testCases := []struct {
		name             string
		defaultValue     any
		enumConstraint   [][]any // Outer slice for varargs, inner for the actual enum values
		expectedReturn   any
	}{
		{
			name:           "string default no enum",
			defaultValue:   "hello",
			enumConstraint: nil,
			expectedReturn: "hello",
		},
		{
			name:           "int default no enum",
			defaultValue:   42,
			enumConstraint: nil,
			expectedReturn: 42,
		},
		{
			name:           "bool default no enum",
			defaultValue:   true,
			enumConstraint: nil,
			expectedReturn: true,
		},
		{
			name:           "string default with string enum",
			defaultValue:   "one",
			enumConstraint: [][]any{{"one", "two", "three"}},
			expectedReturn: "one",
		},
		{
			name:           "int default with int enum",
			defaultValue:   10,
			enumConstraint: [][]any{{10, 20, 30}},
			expectedReturn: 10,
		},
		{
			name:           "string default with empty enum",
			defaultValue:   "test",
			enumConstraint: [][]any{{}}, // Empty enum constraint
			expectedReturn: "test",
		},
		{
			name:           "string default with nil enum constraint (varargs not passed)",
			defaultValue:   "test_nil_constraint",
			enumConstraint: nil, // equivalent to not passing the vararg
			expectedReturn: "test_nil_constraint",
		},
		{
			name:           "string default with nil actual enum slice",
			defaultValue:   "test_nil_slice",
			enumConstraint: [][]any{nil}, // Varargs passed, but the slice itself is nil
			expectedReturn: "test_nil_slice",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var result any
			// Helper to call generic Default with specific types for testing
			callDefault := func(dv any, ec [][]any) any {
				switch val := dv.(type) {
				case string:
					var typedEc [][]string
					if ec != nil {
						typedEc = make([][]string, len(ec))
						for i, subSlice := range ec {
							if subSlice != nil {
								typedEc[i] = make([]string, len(subSlice))
								for j, item := range subSlice { typedEc[i][j] = item.(string) }
							}
						}
					}
					if len(typedEc) > 0 { return Default(val, typedEc[0]) }
					return Default(val)
				case int:
					var typedEc [][]int
					if ec != nil {
						typedEc = make([][]int, len(ec))
						for i, subSlice := range ec {
							if subSlice != nil {
								typedEc[i] = make([]int, len(subSlice))
								for j, item := range subSlice { typedEc[i][j] = item.(int) }
							}
						}
					}
					if len(typedEc) > 0 { return Default(val, typedEc[0]) }
					return Default(val)
				case bool:
					// Enum for bool is less common but testable
					var typedEc [][]bool
					if ec != nil {
						typedEc = make([][]bool, len(ec))
						for i, subSlice := range ec {
							if subSlice != nil {
								typedEc[i] = make([]bool, len(subSlice))
								for j, item := range subSlice { typedEc[i][j] = item.(bool) }
							}
						}
					}
					if len(typedEc) > 0 { return Default(val, typedEc[0]) }
					return Default(val)
				default:
					t.Fatalf("Unsupported type for Default test: %T", dv)
					return nil
				}
			}

			result = callDefault(tc.defaultValue, tc.enumConstraint)

			if !reflect.DeepEqual(result, tc.expectedReturn) {
				t.Errorf("Default(%v, %v) = %v, want %v", tc.defaultValue, tc.enumConstraint, result, tc.expectedReturn)
			}
		})
	}
}
package main

import (
	"bytes"
	"flag"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/podhmo/goat/internal/config"
)

// Helper function to capture stdout and stderr
func captureOutput(f func()) (string, string) {
	var stdoutBuf, stderrBuf bytes.Buffer
	oldStdout := os.Stdout
	oldStderr := os.Stderr
	oldLogOutput := log.Writer()

	rOut, wOut, _ := os.Pipe()
	rErr, wErr, _ := os.Pipe()

	os.Stdout = wOut
	os.Stderr = wErr
	log.SetOutput(wErr) // Capture log output to stderr buffer

	defer func() {
		os.Stdout = oldStdout
		os.Stderr = oldStderr
		log.SetOutput(oldLogOutput)
	}()

	f()

	wOut.Close()
	wErr.Close()

	stdout, _ := io.ReadAll(rOut)
	stderr, _ := io.ReadAll(rErr)

	return string(stdout), string(stderr)
}

// TestMain_runGoat_HelpOutput is more of an integration test for the help generation path.
func TestMain_runGoat_HelpOutput(t *testing.T) {
	// Create a temporary Go file for testing
	tmpDir := t.TempDir()
	tmpFile := filepath.Join(tmpDir, "testapp.go")
	content := `
package main

import "github.com/podhmo/goat/goat"

// Options for testapp.
// This is a test application.
type Options struct {
	// Name of the user.
	Name string %s
	// Port number.
	Port int %s
}

func NewOptions() *Options {
	return &Options{
		Name: goat.Default("anonymous"),
		Port: goat.Default(8080),
	}
}

// Run the test application.
// It does something.
func Run(opts Options) error {
	return nil
}

func main() { /* Will be replaced */ }
`
	// For this test, tags on Options struct are not essential as interpreter handles defaults
	formattedContent := fmt.Sprintf(content, "", "") // No struct tags for this simple test
	if err := os.WriteFile(tmpFile, []byte(formattedContent), 0644); err != nil {
		t.Fatalf("Failed to write temp file: %v", err)
	}

	cfg := &config.Config{
		RunFuncName:            "Run",
		OptionsInitializerName: "NewOptions",
		TargetFile:             tmpFile,
	}

	// Capture output of runGoat
	// We are primarily interested in the "Generated Help Message" part
	// Logs will go to stderr and also be captured.
	stdout, stderr := captureOutput(func() {
		if err := runGoat(cfg); err != nil {
			// In a real CLI, this would log.Fatal. For test, we check err.
			t.Errorf("runGoat returned an error: %v. Stderr: %s", err, stderr)
		}
	})

	// Verify stderr for unexpected errors (logs are OK)
	if strings.Contains(stderr, "Error:") && !strings.Contains(stderr, "runGoat returned an error:") { // don't double count if t.Errorf already fired
		t.Logf("runGoat produced log errors/warnings: %s", stderr)
	}


	// Verify stdout for help message content
	if !strings.Contains(stdout, "-------------------- Generated Help Message --------------------") {
		t.Errorf("Stdout missing generated help message preamble. Stdout:\n%s", stdout)
	}
	if !strings.Contains(stdout, "testapp - Run the test application.") {
		t.Errorf("Help message missing or incorrect command title. Stdout:\n%s", stdout)
	}
	if !strings.Contains(stdout, "--name string") {
		t.Errorf("Help message missing --name flag. Stdout:\n%s", stdout)
	}
	if !strings.Contains(stdout, "Name of the user. (default: \"anonymous\")") {
		t.Errorf("Help message missing or incorrect help for Name. Stdout:\n%s", stdout)
	}
	if !strings.Contains(stdout, "--port int") {
		t.Errorf("Help message missing --port flag. Stdout:\n%s", stdout)
	}
	if !strings.Contains(stdout, "Port number. (default: 8080)") {
		t.Errorf("Help message missing or incorrect help for Port. Stdout:\n%s", stdout)
	}
	// t.Logf("Stdout:\n%s", stdout) // For debugging
}

// Example for CLI usage (can be run with `go test -run ExampleMain_NoArgs`)
func ExampleMain_NoArgs() {
	// Simulate calling main() with no arguments
	oldArgs := os.Args
	defer func() { os.Args = oldArgs }()
	os.Args = []string{"goat"} // Program name only

	// Capture stderr to check usage message
	// We need to also reset flags because they are global
	fs := flag.NewFlagSet("goat", flag.ContinueOnError)
	// Re-define flags from main() on this temporary flagset
	var runFuncName, optionsInitializerName string
	fs.StringVar(&runFuncName, "run", "run", "")
	fs.StringVar(&optionsInitializerName, "initializer", "NewOptions", "")


	var errBuf bytes.Buffer
	fs.SetOutput(&errBuf) // Capture flag parsing errors/usage

	// Manually call main's core logic after flag setup if possible,
	// or test a sub-function. Here, we simulate flag parsing leading to error.
	err := fs.Parse(os.Args[1:]) // This will error due to missing target file
	
	// Since main() calls os.Exit(1), we can't directly call it and check output easily.
	// Instead, we check the expected error from flag parsing when no args are given.
	// In the actual main(), flag.Parse() is called on the global CommandLine.
	// This example demonstrates testing the *behavior* of argument requirement.

	if err == nil && len(os.Args[1:]) == 0 { // If Parse somehow didn't error
		// And if main() logic were here, it would print to Stderr.
		// For this example, we assume flag.Parse() would handle this.
		// The main() function itself prints "Error: Target Go file must be specified."
		// and then flag.Usage().
		fmt.Println("Error: Target Go file must be specified.")
		fmt.Println("Usage: goat [options] <target_gofile.go>")
		fmt.Println("")
		fmt.Println("Options:")
		// Output default flags manually for the example output matching
		fmt.Println("  -initializer string")
		fmt.Println("    \tName of the function that initializes the options struct (e.g., NewOptions) (default \"NewOptions\")")
		fmt.Println("  -run string")
		fmt.Println("    \tName of the function to be treated as the entrypoint (e.g., run(Options) error) (default \"run\")")

	} else if err != nil {
		// If fs.Parse actually errors (which it might not without a defined arg)
		// then the error path in main() that prints usage would be hit.
		// This is a bit tricky to simulate perfectly without refactoring main().
		// We'll assume the "Error: Target Go file..." path from main() is dominant.
		fmt.Println("Error: Target Go file must be specified.") // Expected from main() logic
		fmt.Println("Usage: goat [options] <target_gofile.go>")
		// ... plus default flag output ...
	}


	// Output:
	// Error: Target Go file must be specified.
	// Usage: goat [options] <target_gofile.go>
	//
	// Options:
	//   -initializer string
	//     	Name of the function that initializes the options struct (e.g., NewOptions) (default "NewOptions")
	//   -run string
	//     	Name of the function to be treated as the entrypoint (e.g., run(Options) error) (default "run")
}
package analyzer

import (
	"strings"
	"testing"

	"github.com/podhmo/goat/internal/metadata"
)

// TestAnalyze_Integration is a more integrated test for the analyzer package,
// ensuring AnalyzeRunFunc and AnalyzeOptions work together.
func TestAnalyze_Integration(t *testing.T) {
	content := `
package testapp

import "context"

// Options for the application.
type Options struct {
	// Hostname to connect to.
	Host string %s
	// Port number.
	Port *int %s
}

// Run is the main entrypoint.
// It connects to a server.
func Run(ctx context.Context, opts Options) error {
	return nil
}
`
	// Using Sprintf for tag injection, like in options_analyzer_test
	formattedContent := Sprintf(content, "`env:\"APP_HOST\"`", "`env:\"APP_PORT\"`")
	fileAst := parseTestFile(t, formattedContent) // Assumes parseTestFile helper is available

	cmdMeta, optionsStructName, err := Analyze(fileAst, "Run")
	if err != nil {
		t.Fatalf("Analyze failed: %v", err)
	}

	if cmdMeta == nil {
		t.Fatal("Analyze returned nil CommandMetadata")
	}
	if optionsStructName != "Options" {
		t.Errorf("Expected optionsStructName 'Options', got '%s'", optionsStructName)
	}

	// Check CommandMetadata basics
	if cmdMeta.Name != "testapp" { // Package name
		t.Errorf("Expected command name 'testapp', got '%s'", cmdMeta.Name)
	}
	expectedDesc := "Run is the main entrypoint.\nIt connects to a server."
	if strings.TrimSpace(cmdMeta.Description) != expectedDesc {
		t.Errorf("Expected command description '%s', got '%s'", expectedDesc, cmdMeta.Description)
	}

	// Check RunFuncInfo
	if cmdMeta.RunFunc == nil {
		t.Fatal("CommandMetadata.RunFunc is nil")
	}
	if cmdMeta.RunFunc.Name != "Run" {
		t.Errorf("Expected RunFunc.Name 'Run', got '%s'", cmdMeta.RunFunc.Name)
	}
	if cmdMeta.RunFunc.ContextArgName != "ctx" {
		t.Errorf("Expected RunFunc.ContextArgName 'ctx', got '%s'", cmdMeta.RunFunc.ContextArgName)
	}
	if cmdMeta.RunFunc.OptionsArgName != "opts" {
		t.Errorf("Expected RunFunc.OptionsArgName 'opts', got '%s'", cmdMeta.RunFunc.OptionsArgName)
	}
	if cmdMeta.RunFunc.OptionsArgType != "Options" {
		t.Errorf("Expected RunFunc.OptionsArgType 'Options', got '%s'", cmdMeta.RunFunc.OptionsArgType)
	}


	// Check Options
	if len(cmdMeta.Options) != 2 {
		t.Fatalf("Expected 2 options, got %d", len(cmdMeta.Options))
	}

	hostOpt := cmdMeta.Options[0]
	if hostOpt.Name != "Host" || hostOpt.CliName != "host" || hostOpt.EnvVar != "APP_HOST" || !hostOpt.IsRequired {
		t.Errorf("Host option mismatch: %+v", hostOpt)
	}
	if strings.TrimSpace(hostOpt.HelpText) != "Hostname to connect to." {
		t.Errorf("Host help text mismatch: '%s'", hostOpt.HelpText)
	}


	portOpt := cmdMeta.Options[1]
	if portOpt.Name != "Port" || portOpt.CliName != "port" || portOpt.EnvVar != "APP_PORT" || !portOpt.IsPointer || portOpt.IsRequired {
		t.Errorf("Port option mismatch: %+v", portOpt)
	}
	if strings.TrimSpace(portOpt.HelpText) != "Port number." {
		t.Errorf("Port help text mismatch: '%s'", portOpt.HelpText)
	}
}

func TestAnalyze_RunFuncNotFound(t *testing.T) {
	content := `package main; type Options struct{}; func Other(o Options) error {return nil}`
	fileAst := parseTestFile(t, content)
	_, _, err := Analyze(fileAst, "NonExistentRun")
	if err == nil {
		t.Fatal("Analyze should have failed if run func not found")
	}
	if !strings.Contains(err.Error(), "analyzing run function 'NonExistentRun'") {
		t.Errorf("Unexpected error message: %v", err)
	}
}

func TestAnalyze_OptionsStructNotFound(t *testing.T) {
	content := `package main; func Run(opts NonExistentOptions) error {return nil}`
	fileAst := parseTestFile(t, content)
	_, _, err := Analyze(fileAst, "Run")
	if err == nil {
		t.Fatal("Analyze should have failed if options struct not found")
	}
	// This error comes from AnalyzeOptions, wrapped by Analyze
	if !strings.Contains(err.Error(), "options struct type 'NonExistentOptions' not found") {
		t.Errorf("Unexpected error message: %v", err)
	}
}

// Sprintf is a local helper, normally you'd import "fmt"
func Sprintf(format string, args ...any) string {
	return fmt.Sprintf(format, args...)
}

// parseTestFile helper should be defined (e.g., from options_analyzer_test.go or a shared test util)
// For brevity, assuming it exists. If not, copy from options_analyzer_test.go:
// func parseTestFile(t *testing.T, content string) *ast.File { ... }

(返答終了理由: STOP)

メタデータ

{
  "runSettings": {
    "temperature": 1,
    "model": "models/gemini-2.5-pro-preview-05-06",
    "topP": 0.95,
    "topK": 64,
    "maxOutputTokens": 65536,
    "safetySettings": [
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "OFF"
      }
    ],
    "responseMimeType": "text/plain",
    "enableCodeExecution": false,
    "enableSearchAsATool": false,
    "enableBrowseAsATool": false,
    "enableAutoFunctionResponse": false
  },
  "systemInstruction": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment