Created
November 28, 2024 15:23
-
-
Save tk0miya/fd766d10af26cc3a0fd2bf3997b6498d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# ERB ファイルの型チェックをするための実験コード | |
# | |
# 適当な場所に保存し、Steepfile の先頭で `require_relative 'lib/steep_ext'` のようにロードする。 | |
# | |
# なお、ERB の型チェックを行うには、各 .erb ファイルの self を差し替える必要がある。 | |
# そのために、各 .erb ファイルに proc { ... }.call というブロックを差し込み、 | |
# ブロックの先頭に `# @type self:` コメントでクラス名やモジュール名を指定する。 | |
# | |
# <% proc { %> | |
# <%# @type self: Views::Admin::Index %> | |
# ... | |
# <% }.call %> | |
# | |
# また、上記の self 型に対応するクラスを用意する。 | |
# 手元では、以下のような ActionView::Base のサブクラスを用意し、適切なメソッド、インスタンス変数の型を定義している。 | |
# | |
# module Views | |
# module Admin | |
# class Index < ActionView::Base | |
# include _RbsRailsPathHelpers | |
# @instance_var: String | |
# end | |
# end | |
# end | |
# | |
# | |
# なお、VSCode を利用している場合はホバーなどの UI まわりの機能は動作しない。 | |
# steep-vscode 拡張が指定する documentSelector に "erb" が含まれていないため、 | |
# .erb ファイルに対する操作が Steep サーバまで伝搬しないためである。 | |
# https://github.com/soutaro/steep-vscode/blob/master/src/extension.ts#L114 | |
module Steep | |
class Project | |
class Pattern | |
alias initialize_orig initialize | |
def initialize(patterns:, ext:, ignores: []) | |
# Steep は .rb ファイルのみを対象としているので .erb ファイルを対象とするように上書きする。 | |
# 本来は Steep::Project::DSL をハックするべきだが、Steepfile 経由では書き換えができない (すでに実行中である) ため、 | |
# Steep::Project::Pattern の初期化をフックする。 | |
# | |
# なお、この ext オプションは Steep::Project::Pattern の判定や Steep::Services::FileLoader などでも利用されている | |
ext = '.erb' if ext == '.rb' | |
initialize_orig(patterns:, ext:, ignores:) | |
end | |
end | |
end | |
module Services | |
class TypeCheckService | |
class SourceFile | |
alias initialize_orig initialize | |
def initialize(path:, node:, content:, typing:, ignores:, errors:) # rubocop:disable Metrics/ParameterLists | |
initialize_orig(path:, node:, content:, typing:, ignores:, errors:) | |
@converted = ErbExtractor.new.extract(path:, text: content) if path.extname == '.erb' && content | |
end | |
def content | |
@converted || @content | |
end | |
end | |
end | |
end | |
end | |
class ErbExtractor | |
def extract(path:, text:) #: String | |
require 'parser' | |
require 'better_html' | |
require 'better_html/parser' | |
buffer = Parser::Source::Buffer.new(path.to_s, source: text) | |
parser = BetterHtml::Parser.new(buffer, template_language: :html) | |
traverse(parser.ast).map { |node| whiten(node:, buffer:) }.join | |
end | |
#: (untyped?) -> Enumerator[String | untyped] | |
#: (untyped?) { (String | untyped) -> void } -> void | |
def traverse(node, &block) | |
return to_enum(__method__, node) unless block | |
return if node.nil? | |
if node.is_a?(String) || node.type == :tag || node.type == :erb | |
# @type node: String | untyped | |
yield node | |
else | |
node.children.each do |child| | |
traverse(child, &block) | |
end | |
end | |
end | |
# @rbs node: String | untyped | |
# @rbs buffer: untyped | |
def whiten(node:, buffer:) #: String # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength | |
case node | |
when String | |
node.gsub(/\S/, ' ') | |
when AST::Node, BetterHtml::AST::Node | |
case node.type | |
when :erb | |
prefix, _, code, = node.children | |
case prefix&.children&.first | |
when '#' | |
"# #{code.children.first} " | |
when '=' | |
" #{code.children.first} " | |
when '==' | |
" #{code.children.first} " | |
else | |
" #{code.children.first} " | |
end | |
when :tag | |
text = buffer.source[node.location.to_range] | |
text.gsub(/\S/, ' ') | |
else | |
Steep.logger.info("Unknown erb node type: #{node}") | |
raise | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment