【Python】1つのファイル内における関数の依存関係をMermaidの書式で出力する

Code

はじまり

135ml
135ml

よーし、出来たぜえ・・・

リサちゃん
リサちゃん

お、何か作ったんかい?

135ml
135ml

ああ、昨日作ったスクリプトの関数の依存関係を示すクラス図を作りたかったんだが、

その時短になるスクリプトが出来たんだぜ。

リサちゃん
リサちゃん

じゃあ、早速教えてくれい。

スクリプトの概要

環境

まず、動作確認した環境は以下のとおりです。

  • Python 3.9.7
  • macOS Monterey
  • 対応しているファイルの拡張子:PythonもしくはJavaScript(Google Apps Scriptとか基本的なJavaScriptの文法であれば行けると思います。)

何をするのか

ざっくり言うと、ソースの中を読み取って、どの関数がどの関数を参照しているかどうかをクラスダイアグラムとして、出力してくれます。

それでは、1つのファイルを対象にスクリプトを動かします。

本機能はPyPIに上げていないので、こんな感じでインストールして、

pip install git+https://github.com/landmaster135/landmasterlibrary.git

Pythonが実行できるディレクトリで以下のような感じで、実行します。printdependsonmdは、setup.py内で'console_scripts'に登録されているものです。

printdependsonmd '/Users/username/Downloads/BlogManagerSheet-main/youtube.gs' '.js'

実行すると、ターミナル上にMermaidの文が出力されます。

============ depends list on Markdown: start ============
```mermaid
classDiagram
  getSpreadsheet <|-- getSheet
  getSheet <|-- getSheet
  getSheet <|-- getIssuesFromGss
  isPostedIssue <|-- getIssuesFromGss
  getDatesByRecords <|-- makeFoldersYetExist
  getDatesByRecords <|-- makeFoldersYetExist
  getDatesByRecords <|-- makeFoldersYetExist
  getDatesByRecords <|-- makeFoldersYetExist
  getDatesNotExist <|-- makeFoldersYetExist
  getDatesNotExist <|-- makeFoldersYetExist
  getDatesNotExist <|-- makeFoldersYetExist
  getDatesNotExist <|-- makeFoldersYetExist
  makeFolderByDates <|-- makeFoldersYetExist
  makeFolderByDates <|-- makeFoldersYetExist
  makeFolderByDates <|-- makeFoldersYetExist
  makeFolderByDates <|-- makeFoldersYetExist
  getIssuesFromGss <|-- getPartsOfIssues
  getIssuesFromGss <|-- getPartsOfIssues
  getIssuesFromGss <|-- getPartsOfIssues
  getIssuesFromGss <|-- getPartsOfIssues
  getPartsOfRecords <|-- getPartsOfIssues
  getPartsOfRecords <|-- getPartsOfIssues
  getPartsOfRecords <|-- getPartsOfIssues
  getPartsOfRecords <|-- getPartsOfIssues
  getPartsOfIssues <|-- makeFoldersIntoDrive
  getPartsOfIssues <|-- makeFoldersIntoDrive
  getPartsOfIssues <|-- makeFoldersIntoDrive
  getPartsOfIssues <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  makeFoldersYetExist <|-- makeFoldersIntoDrive
  getFolderMovingInfo <|-- getFolderMovingInfo
  getFolderMovingInfo <|-- getFolderMovingInfo
  moveFoldersForYoutube <|-- moveFoldersForYoutube
  getDatesAlreadyPosted <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  getFolderMovingInfo <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  getFolderMovingInfo <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  getFolderMovingInfo <|-- moveFoldersAlreadyPosted
  getFolderMovingInfo <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  moveFoldersForYoutube <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  moveFoldersForYoutube <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  moveFoldersForYoutube <|-- moveFoldersAlreadyPosted
  moveFoldersForYoutube <|-- moveFoldersAlreadyPosted
  moveFoldersAlreadyPosted <|-- moveFoldersAlreadyPosted
  getPartsOfRecords <|-- manageFoldersInDrive
  getPartsOfRecords <|-- manageFoldersInDrive
  getPartsOfIssues <|-- manageFoldersInDrive
  getPartsOfRecords <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  moveFoldersAlreadyPosted <|-- manageFoldersInDrive
  class getSpreadsheet{
  }
  class getSheet{
  }
  class isPostedIssue{
  }
  class getIssuesFromGss{
  }
  class getPartsOfRecords{
  }
  class getDatesByRecords{
  }
  class getDatesNotExist{
  }
  class makeFolderByDates{
  }
  class makeFoldersYetExist{
  }
  class getPartsOfIssues{
  }
  class makeFoldersIntoDrive{
  }
  class getDatesAlreadyPosted{
  }
  class getFolderMovingInfo{
  }
  class moveFoldersForYoutube{
  }
  class moveFoldersAlreadyPosted{
  }
  class manageFoldersInDrive{
  }
```
============ depends list on Markdown: end ============

出力されたMermaidをREADME.mdに記述して、GitHubで表示するとこんな感じになります。

一旦上記のスクリプトを作ってクラス図にすることで、久しぶりにスクリプトを直すときに参照関係が一目で分かるようになったので、直すのが楽になりました。

何をやっているのか

処理の流れは以下の通りになっています。

ファイルから関数を取得する。(Pythonだと'def '、JavaScriptだと'function 'を目印にして取得する。)

その関数の中から、ファイルの中の関数がないかどうかを探す。

依存関係を保持した辞書型オブジェクトを出力する。(ex. { '参照元': ['参照先A', '参照先B', ...] }

辞書型オブジェクトからMermaid表記に出力する。(ex. 参照先 <|-- 参照元

最も煩雑だったところは、「その関数の中から、ファイルの中の関数がないかどうかを探す」〜「依存関係を保持した辞書型オブジェクトを出力する。」の部分でした。

いつから参照元の関数を切り替えるか、関数の記述ではないところを判断する部分はどこになるかなどを少し長めに考えました。

宣言部分は関数を探さないことにしたら、コードが綺麗になりました。(予約語'continue'はすごい便利ですよね。)

スクリプトで悩ましいところ

ひとまずの目的は満たせたので良かったのですが、このスクリプトにおいて、以下の点が悩ましい・・・

  • コメントや文字列の中にある関数名も拾ってきてしまう。
  • 自分の関数も取ってきてしまう。
  • 関数の中で複数回呼び出される場合、その回数分だけ参照関係を出力する。
  • 他のファイルとの参照関係は取れない。
  • モックやフィクスチャが入っていた場合を対応していない。

コメントや文字列の中にある関数名も拾ってきてしまう。

処理として、hogehoge(fuga)みたいに関数内で使用されている場合は、参照先として捕捉して良いのですが、console.log(`hogehoge(fuga)`)とされていたり、ロングテキストによるコメントの中などで関数名を使用されていた場合は、その関数を無視していいかどうかが一概には言えないと思っています。

console.log(`hogehoge(fuga)`)の場合は、目印として関数名を記述しておきたいので参照しているとみなしたいこともあるかと思います。

ロングテキストによるコメントによると、改行されていると前後の行のクォーテーションを読まなければならなくなるので、実装するのがかなり大変になります・・・

自分の関数も取ってきてしまう。

これに関しても一概には言えず、ログ取得の際に自分の関数名を取得する意図があるかもしれませんし、再帰関数である可能性もあります。

そのため、自分を参照している場合は参照先から外すということも考えましたが、やめておきました。

関数の中で複数回呼び出される場合、その回数分だけ参照関係を出力する。

概要で出力したクラス図をご覧の通り、いくつかの関数から同じ関数に向けて複数本の矢印が伸びているのが確認できるかと思います。

この場合に、矢印の本数を1本にするようにした方が良いのかどうか悩みましたが、矢印の本数を複数本見せることで修正箇所が何箇所あるかどうかも確認できるようになっているので、そのまま、複数本の矢印を描画する状態にしておきました。

これは、先程挙げた「コメントや文字列の中にある関数名も拾ってきてしまう。」の点とも絡むところになります。

他のファイルとの参照関係は取れない。

あくまで、今回のスクリプトの実装範囲としては、「1つの」ファイル内の参照関係なので、他のファイルからインポートした関数の参照関係は描画できません。

出来たら更に便利なんですけどね・・・

しかし、実装にとても手間がかかりますね。コードの静的解析ツールを作るのってなかなか骨が折れるということが今回分かりました。

モックやフィクスチャが入っていた場合を対応していない。

主にテストコードが記述されているファイルに使用した場合、モックやフィクスチャが沢山入っていると思うので、そこに対応していないんですよね・・・

参照元の関数を切り替えたら、その宣言部分の行の前後を見ることになるのかな。実装が険しいですねえ・・・

おしまい

135ml
135ml

結構、抜けているところもあるけど、一旦は役立つツールになったので、よしとします。

リサちゃん
リサちゃん

Mermaidは、図をコードで管理できるのは良いけど、そのコードを打ち込むのが大変だから、その一助になるのはとても有り難いでやんす。

135ml
135ml

以下のリポジトリのsrc/landmasterlibrary/generaltool.pyというファイルの中に処理は入っています。ご参考ください。

GitHub - landmaster135/landmasterlibrary
Contribute to landmaster135/landmasterlibrary development by creating an account on GitHub.

以上になります!

コメント

タイトルとURLをコピーしました