Open11

ChatGPT APIでソースコードレビュー

erukitierukiti

やりたいことはソースコードのlintをeslintでやるのも限界なので、ChatGPTちゃんの知性を頼りたい。より具体的には Markdown ドキュメントでコーディングルールを与えて、そのコーディングルールに沿っていないコードをあぶり出したい。

import * as mod from 'https://deno.land/std/dotenv/mod.ts';
import { resolve } from 'https://deno.land/std/path/mod.ts';
import { walk } from 'https://deno.land/std/fs/mod.ts';

import { Message, completeChat } from './llm.ts';

const envPath = resolve(`${Deno.env.get('HOME')}/.gen.env`);

const { OPENAI_API_KEY: apiKey } = await mod.load({ envPath });

const readProjectFiles = async (dir: string) => {
  const files: { [props: string]: string } = {};

  for await (const entry of walk(dir)) {
    if (entry.isFile) {
      const file = await Deno.readTextFile(entry.path);
      files[entry.path] = file;
    }
  }
  return files;
};

const files = await readProjectFiles('./');

const findFromFiles = (
  files: { [props: string]: string },
  func: (path: string) => boolean
) => {
  const result: { [props: string]: string } = {};
  for (const key in files) {
    if (func(key)) {
      result[key] = files[key];
    }
  }
  return result;
};

const documentFiles = findFromFiles(files, (path) => path.endsWith('.md'));
const soruceCodeFiles = findFromFiles(files, (path) => path.endsWith('.ts'));

const document = Object.keys(documentFiles)
  .map(
    (path) =>
      `### ${path}\n\n\`\`\`${path.split('.').pop()}\n${documentFiles[
        path
      ].trim()}\n\`\`\`\n\n}`
  )
  .join('\n\n');

const soruceCode = Object.keys(soruceCodeFiles)
  .map(
    (path) =>
      `### ${path}\n\n\`\`\`${path.split('.').pop()}\n${soruceCodeFiles[path]
        .trim()
        .replaceAll('\\', '\\\\')
        .replaceAll('\n', '\\n')}\n\`\`\`\n\n}`
  )
  .join('\n\n');

const messages: Message[] = [
  {
    role: 'system',
    content: `あなたはlinterです。プロジェクトのファイルをjsonで示すので、lintを行ってください。

## ルール

* ドキュメントの中でコーディングルールが定義されている場合、必ずそれに従ってlintしてください。
* 問題がない場合は言及しなくても構いません。
* lint結果は、Markdown形式で、ファイルごとに \`###\` で区切ってください。
* 書式に関するlintはしないでください。たとえばセミコロンの有無、インデントの深さ、改行の有無など

## プロジェクトのドキュメント

${document}

## プロジェクトのソースコード

${soruceCode}
`,
  },
];

const res = await completeChat(apiKey, messages);
console.log(res?.content);

README.md

# ChatGPT linter

ChatGPT linterはChat completions APIを使ってプロジェクトのlintを行うツールです。

## コーディングルール

* 関数の型の戻り値を省略しないこと
erukitierukiti

エスケープの仕方がまずいのか微妙な指摘が来る時がある

$ deno run --allow-net --allow-env --allow-read main.ts
### main.ts

* `import`文の後に改行がないため、改行を追加してください。
* `readProjectFiles`関数の引数には、ディレクトリパスを渡すことが期待されていますが、現在は`'./'`という文字列が渡されています。このため、関数内で`Deno.readTextFile`を実行するとエラーが発生する可能性があります。引数を修正してください。
* `findFromFiles`関数の引数`func`は、ファイルパスを受け取って真偽値を返す関数であることが期待されていますが、現在はファイル名を受け取って真偽値を返す関数になっています。引数名を`path`から`fileName`に変更し、関数内でファイルパスを生成するように修正してください。
* `documentFiles`と`soruceCodeFiles`の定義で、`Object.keys`を使用しているため、ファイルの順序が保証されません。ファイルの順序を保証するためには、`Object.entries`を使用する必要があります。
* `document`と`soruceCode`の定義で、改行コードを`\n`から`\\n`にエスケープしていますが、これは不要です。元の改行コードをそのまま使用してください。

### llm.ts

特に問題ありません。
erukitierukiti
## プロジェクトのドキュメント

\`\`\`json
${JSON.stringify(documentFiles)}
\`\`\`

## プロジェクトのソースコード

\`\`\`json
${JSON.stringify(soruceCodeFiles)}
\`\`\`

の方が安定する気がしてる

erukitierukiti
import * as mod from 'https://deno.land/std/dotenv/mod.ts';
import { resolve } from 'https://deno.land/std/path/mod.ts';
import { walk } from 'https://deno.land/std/fs/mod.ts';

import { Message, completeChat } from './llm.ts';

const home = Deno.env.get('HOME');

if (!home) {
  throw new Error('HOME not found');
}

const envPath = resolve(`${home}/.gen.env`);

const { OPENAI_API_KEY: apiKey } = await mod.load({ envPath });

const readProjectFiles = async (dir: string) => {
  const files: { [props: string]: string } = {};

  for await (const entry of walk(dir)) {
    if (entry.isFile) {
      const file = await Deno.readTextFile(entry.path);
      files[entry.path] = file;
    }
  }
  return files;
};

const files = await readProjectFiles('./');

const findFromFiles = (
  files: { [props: string]: string },
  func: (path: string) => boolean
) => {
  const result: { [props: string]: string } = {};
  for (const key in files) {
    if (func(key)) {
      result[key] = files[key];
    }
  }
  return result;
};

const documentFiles = findFromFiles(files, (path) => path.endsWith('.md'));
const sourceCodeFiles = findFromFiles(files, (path) => path.endsWith('.ts'));

const messages: Message[] = [
  {
    role: 'system',
    content: `あなたはコードレビューアです。プロジェクトのファイルをjsonで示すのでコードレビューしてください。

## ルール

* ドキュメントの中でコーディングルールが定義されている場合、必ずそれに従ってレビューしてください。
* 問題がない場合は言及しないでください。
* lint結果は、Markdown形式で、ファイルごとに \`###\` で区切ってください。
* 書式に関するlintはしないでください。たとえばセミコロンの有無、インデントの深さ、改行の有無など

## プロジェクトのドキュメント

\`\`\`json
${JSON.stringify(documentFiles)}
\`\`\`

## プロジェクトのソースコード

\`\`\`json
${JSON.stringify(sourceCodeFiles)}
\`\`\`

`,
  },
];

const res = await completeChat(apiKey, messages);
console.log(res?.content);

これでファイルの与え方は安定したと思う。というか元々JSONで与えるためのプロンプトにしてたわ・・・。

erukitierukiti

コーディングルールに沿って指摘をしてくれない・・・。戻り値の型指定してないんだよぉ

erukitierukiti
あなたはコードレビューアです。プロジェクトのファイルをコードレビューしてください。

## ルール

* プロジェクトのファイルはJSONで示されています。
* まずはコーディングルールを抽出して、そのコーディングルールに従ってレビューしてください。

のプロンプトだと、明確に従ってくれるようになったぞ!

$ deno run --allow-net --allow-env --allow-read main.ts
コーディングルール:

* 関数の型の戻り値を省略しないこと

レビュー結果:

* `main.ts`の`readProjectFiles`関数と`findFromFiles`関数の戻り値の型が明示されていないため、戻り値の型を明示する必要があります。
* `main.ts`の`findFromFiles`関数の引数の型が明示されていないため、引数の型を明示する必要があります。
* `llm.ts`の`completeChat`関数の引数の型が明示されていないため、引数の型を明示する必要があります。
* `llm.ts`の`completeChat`関数の戻り値の型が明示されていないため、戻り値の型を明示する必要があります。
* `llm.ts`の`completeChat`関数の`body`変数の型が明示されていないため、型を明示する必要があります。
* `llm.ts`の`completeChat`関数の`data`変数の型が明示されていないため、型を明示する必要があります。
* `llm.ts`の`completeChat`関数の`choice`変数の型が明示されていないため、型を明示する必要があります。

ちょっと違うけど!

erukitierukiti
あなたはコードレビューアです。プロジェクトのファイルをコードレビューしてください。

## ルール

* プロジェクトのドキュメントとソースコードがJSONで記述されています。
* まずはコーディングルールを抽出して、そのコーディングルールに従ってレビューしてください。
* 次にソースコードの複雑さを算出して、それに沿ってレビューしてください。
* フォーマットに関するレビューは不要です。たとえばセミコロンの有無、インデントの深さ、改行の有無など

ちょっと試しにソースコードの複雑さについてためしてるが、JSONの複雑さを算出できないとか言い出した。

erukitierukiti

複雑さだけを算出するプロンプトが作れないものか。

`あなたはコードレビューアです。プロジェクトのファイルをコードレビューするために、ソースコードの複雑さを評価してください。

## プロジェクトのソースコード

${sourceCodes}

`,

なんかいけそう

このプロジェクトは、Denoを使用しています。main.tsは、プロジェクト内のすべてのTypeScriptファイルを読み込み、それらをMarkdown形式で出力します。また、OpenAIのAPIを使用して、ユーザーとの対話を補完するためのllm.tsファイルが含まれています。

main.tsのコードは、次のように構成されています。

1. 必要なモジュールをインポートする
2. プロジェクト内のすべてのTypeScriptファイルを読み込む
3. TypeScriptファイルのうち、ソースコードファイルのみを抽出する
4. ソースコードをMarkdown形式で出力する
5. OpenAIのAPIを使用して、ユーザーとの対話を補完する

llm.tsのコードは、次のように構成されています。

1. メッセージオブジェクトの型を定義する
2. メッセージを補完するための関数を定義する

このプロジェクトの複雑さは、比較的低いと言えます。main.tsは、ファイルを読み込んで処理するだけであり、llm.tsは単純な関数を定義するだけです。ただし、OpenAIのAPIを使用しているため、セキュリティ上の懸念があることに注意してください。
erukitierukiti

ある程度シンプルなプロンプトに戻したけど、そもそも「関数の戻り値には型をつけること」というルールを解釈するのは苦手そう

まぁこういうのは typescript コンパイラとか eslint に任せる方が確実かもしれない。

erukitierukiti

別のコーディングルールを指定してみた。

* main.ts以外に処理を記述して、main.tsではそれを呼び出すだけにすること
## コーディングルール

* main.ts以外に処理を記述して、main.tsではそれを呼び出すだけにすること

## コードレビュー結果

* `import`文の並び順が統一されていない
* `readProjectFiles`関数の引数には、ディレクトリのパスを渡すことが期待されるが、関数名からはそのことがわかりにくい
* `findFromFiles`関数の引数には、ファイルのオブジェクトを渡すことが期待されるが、関数名からはそのことがわかりにくい
* `sourceCodes`の生成において、テンプレートリテラル内での改行が正しく処理されていない
* `completeChat`関数の戻り値の型が`Promise<Message | undefined>`であるが、`undefined`を返すことはないため、`Promise<Message>`に変更することが望ましい

以上の点を改善することで、より読みやすく、保守性の高いコードになると考えられます。

ぐぬぬ、コーディングルールガン無視や