misogi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Misogi
4
+ module Rule
5
+ # RSpecの規約に従ってspecファイルとテスト対象の対応をチェックするルール
6
+ # 例:
7
+ # spec/models/user_spec.rb -> Userクラスのテストを含むべき
8
+ # spec/services/admin/user_creator_spec.rb -> Admin::UserCreatorクラスのテストを含むべき
9
+ class RSpec < Base
10
+ def initialize
11
+ super
12
+ @active_support_available = check_active_support
13
+ end
14
+
15
+ # RSpecのディレクトリとソースディレクトリのマッピング
16
+ DIRECTORY_MAPPINGS = {
17
+ "spec/models" => "app/models",
18
+ "spec/controllers" => "app/controllers",
19
+ "spec/helpers" => "app/helpers",
20
+ "spec/mailers" => "app/mailers",
21
+ "spec/jobs" => "app/jobs",
22
+ "spec/services" => "app/services",
23
+ "spec/decorators" => "app/decorators",
24
+ "spec/presenters" => "app/presenters",
25
+ "spec/validators" => "app/validators",
26
+ "spec/policies" => "app/policies",
27
+ "spec/channels" => "app/channels",
28
+ "spec/mailboxes" => "app/mailboxes",
29
+ "spec/lib" => "lib"
30
+ }.freeze
31
+
32
+ # @param file_path [String] 検証対象のファイルパス
33
+ # @param parsed_content [ParsedContent] パース結果(RSpecでは使用しない)
34
+ # @return [Array<Violation>] 検出された違反のリスト
35
+ def validate(file_path, _parsed_content)
36
+ return [] unless file_path.start_with?("spec/") && file_path.end_with?("_spec.rb")
37
+ return [] unless File.exist?(file_path)
38
+
39
+ content = File.read(file_path)
40
+ violations = []
41
+
42
+ # ファイル内容からdescribeの対象を抽出
43
+ described_namespaces = extract_described_namespaces(content)
44
+
45
+ if described_namespaces.empty?
46
+ violations << violation(
47
+ file_path: file_path,
48
+ message: "RSpec.describe または describe が見つかりません"
49
+ )
50
+ return violations
51
+ end
52
+
53
+ # 各describe対象について期待されるspecファイルパスを計算
54
+ spec_base_path = find_spec_base_path(file_path)
55
+ return [] unless spec_base_path
56
+
57
+ expected_paths = described_namespaces.map do |namespace|
58
+ namespace_to_spec_path(namespace, spec_base_path)
59
+ end
60
+
61
+ # 実際のファイルパスが期待されるパスのいずれかと一致するか確認
62
+ unless expected_paths.include?(file_path)
63
+ expected_paths_str = expected_paths.map { |p| "`#{p}`" }.join(" または ")
64
+ described_str = described_namespaces.join(", ")
65
+ violations << violation(
66
+ file_path: file_path,
67
+ message: "テスト対象 '#{described_str}' のspecファイルは #{expected_paths_str} に配置すべきです"
68
+ )
69
+ end
70
+
71
+ violations
72
+ end
73
+
74
+ private
75
+
76
+ # ファイル内容からdescribeの対象(テスト対象のクラス/モジュール)を抽出
77
+ # @param content [String] ファイルの内容
78
+ # @return [Array<String>] 抽出された名前空間のリスト
79
+ def extract_described_namespaces(content)
80
+ namespaces = []
81
+
82
+ # RSpec.describe ClassName または describe ClassName の形式を抽出
83
+ # 定数名(::で区切られた大文字始まりの識別子)をキャプチャ
84
+ pattern = /(?:RSpec\.)?describe\s+([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*)/
85
+
86
+ content.scan(pattern) do |match|
87
+ namespaces << match[0]
88
+ end
89
+
90
+ namespaces.uniq
91
+ end
92
+
93
+ # specファイルのベースパスを見つける
94
+ # @param file_path [String] specファイルのパス
95
+ # @return [String, nil] ベースパス
96
+ def find_spec_base_path(file_path)
97
+ DIRECTORY_MAPPINGS.each_key do |spec_path|
98
+ return spec_path if file_path.start_with?(spec_path)
99
+ end
100
+ nil
101
+ end
102
+
103
+ # 名前空間から期待されるspecファイルパスを生成する
104
+ # @param namespace [String] 名前空間(例: "Foo::Bar")
105
+ # @param spec_base_path [String] specのベースパス(例: "spec/models")
106
+ # @return [String] 期待されるspecファイルパス
107
+ def namespace_to_spec_path(namespace, spec_base_path)
108
+ # 名前空間をパーツに分割
109
+ parts = namespace.split("::")
110
+
111
+ # 各パーツをスネークケースに変換
112
+ snake_parts = parts.map { |part| underscore(part) }
113
+
114
+ # spec_base_pathと結合して_spec.rbを追加
115
+ "#{File.join(spec_base_path, *snake_parts)}_spec.rb"
116
+ end
117
+
118
+ # ActiveSupportが利用可能かチェック
119
+ # @return [Boolean] ActiveSupportが利用可能かどうか
120
+ def check_active_support
121
+ # Rails環境が読み込まれている場合は、ActiveSupportも利用可能
122
+ return true if defined?(::Rails)
123
+
124
+ # Rails環境がない場合は、ActiveSupportを直接読み込んでみる
125
+ require "active_support/inflector"
126
+ true
127
+ rescue LoadError
128
+ false
129
+ end
130
+
131
+ # キャメルケースをスネークケースに変換
132
+ # @param str [String] キャメルケースの文字列
133
+ # @return [String] スネークケースの文字列
134
+ def underscore(str)
135
+ if @active_support_available
136
+ # ActiveSupportが利用可能な場合はそれを使用(inflectionsを考慮)
137
+ ActiveSupport::Inflector.underscore(str)
138
+ else
139
+ # フォールバック: 単純な変換
140
+ str
141
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
142
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
143
+ .downcase
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Misogi
4
+ module Rule
5
+ # Ruby一般の規約に従ってファイルパスとクラス/モジュール名の対応をチェックするルール
6
+ # 例:
7
+ # lib/foo/bar.rb -> Foo::Bar
8
+ # lib/foo.rb -> Foo
9
+ class RubyStandard < Base
10
+ # @param base_path [String] 基準となるディレクトリパス(デフォルト: "lib")
11
+ def initialize(base_path: "lib")
12
+ super()
13
+ @base_path = base_path
14
+ end
15
+
16
+ # @param file_path [String] 検証対象のファイルパス
17
+ # @param parsed_content [ParsedContent] パース結果
18
+ # @return [Array<Violation>] 検出された違反のリスト
19
+ def validate(file_path, parsed_content)
20
+ return [] unless file_path.start_with?(@base_path)
21
+
22
+ violations = []
23
+
24
+ if parsed_content.empty?
25
+ violations << violation(
26
+ file_path: file_path,
27
+ message: "ファイルにクラスまたはモジュールが定義されていません"
28
+ )
29
+ return violations
30
+ end
31
+
32
+ # 定義されている各名前空間について期待されるパスを計算
33
+ expected_paths = parsed_content.namespaces.map do |namespace|
34
+ namespace_to_path(namespace)
35
+ end
36
+
37
+ # 実際のファイルパスが期待されるパスのいずれかと一致するか確認
38
+ unless expected_paths.include?(file_path)
39
+ expected_paths_str = expected_paths.map { |p| "`#{p}`" }.join(" または ")
40
+ defined_namespaces = parsed_content.namespaces.join(", ")
41
+ violations << violation(
42
+ file_path: file_path,
43
+ message: "名前空間 '#{defined_namespaces}' は #{expected_paths_str} に配置すべきです"
44
+ )
45
+ end
46
+
47
+ violations
48
+ end
49
+
50
+ private
51
+
52
+ # 名前空間から期待されるファイルパスを生成する
53
+ # @param namespace [String] 名前空間(例: "Foo::Bar")
54
+ # @return [String] 期待されるファイルパス
55
+ def namespace_to_path(namespace)
56
+ # 名前空間をパーツに分割
57
+ parts = namespace.split("::")
58
+
59
+ # 各パーツをスネークケースに変換
60
+ snake_parts = parts.map { |part| underscore(part) }
61
+
62
+ # base_pathと結合して.rbを追加
63
+ "#{File.join(@base_path, *snake_parts)}.rb"
64
+ end
65
+
66
+ # キャメルケースをスネークケースに変換
67
+ # @param str [String] キャメルケースの文字列
68
+ # @return [String] スネークケースの文字列
69
+ def underscore(str)
70
+ str
71
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
72
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
73
+ .downcase
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Misogi
4
+ # ファイルに対してルールを適用し、違反を検出するクラス
5
+ class Validator
6
+ attr_reader :rules, :parser
7
+
8
+ # @param rules [Array<Rule::Base>] 適用するルールのリスト
9
+ # @param parser [Parser::Base] 使用するパーサー(デフォルト: Parser::Ruby)
10
+ def initialize(rules: [], parser: Parser::Ruby.new)
11
+ @rules = rules
12
+ @parser = parser
13
+ end
14
+
15
+ # ファイルを検証する
16
+ # @param file_path [String] 検証対象のファイルパス
17
+ # @return [Array<Violation>] 検出された違反のリスト
18
+ def validate_file(file_path)
19
+ return [] unless parser.parsable?(file_path)
20
+ return [] unless File.exist?(file_path)
21
+
22
+ content = File.read(file_path)
23
+ parsed_content = parser.parse(content)
24
+
25
+ violations = []
26
+ rules.each do |rule|
27
+ violations.concat(rule.validate(file_path, parsed_content))
28
+ end
29
+
30
+ violations
31
+ end
32
+
33
+ # 複数のファイルを検証する
34
+ # @param file_paths [Array<String>] 検証対象のファイルパスのリスト
35
+ # @return [Array<Violation>] 検出された違反のリスト
36
+ def validate_files(file_paths)
37
+ file_paths.flat_map { |file_path| validate_file(file_path) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Misogi
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Misogi
4
+ # ファイルパスとコンテンツの検証違反を表すクラス
5
+ class Violation
6
+ attr_reader :file_path, :message, :rule_name
7
+
8
+ # @param file_path [String] 違反が見つかったファイルパス
9
+ # @param message [String] 違反の詳細メッセージ
10
+ # @param rule_name [String] 違反を検出したルール名
11
+ def initialize(file_path:, message:, rule_name:)
12
+ @file_path = file_path
13
+ @message = message
14
+ @rule_name = rule_name
15
+ end
16
+
17
+ # 違反情報を文字列として表現
18
+ # @return [String]
19
+ def to_s
20
+ "#{file_path}: [#{rule_name}] #{message}"
21
+ end
22
+ end
23
+ end
data/lib/misogi.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 基本的な型定義は即座にロード
4
+ require_relative "misogi/version"
5
+ require_relative "misogi/violation"
6
+ require_relative "misogi/parsed_content"
7
+
8
+ # Misogiはファイルの内容を解析して、ファイル名やディレクトリ配置が適切かをチェックするlintツールを提供します
9
+ module Misogi
10
+ class Error < StandardError; end
11
+
12
+ # 遅延ロードするクラス/モジュールをautoloadで定義
13
+ autoload :CLI, "misogi/cli"
14
+ autoload :Configuration, "misogi/configuration"
15
+ autoload :Validator, "misogi/validator"
16
+
17
+ # ファイルの内容を解析するパーサー
18
+ module Parser
19
+ autoload :Base, "misogi/parser/base"
20
+ autoload :Ruby, "misogi/parser/ruby"
21
+ end
22
+
23
+ # ファイルパスとコード内容の整合性をチェックするルール
24
+ module Rule
25
+ autoload :Base, "misogi/rule/base"
26
+ autoload :RubyStandard, "misogi/rule/ruby_standard"
27
+ autoload :Rails, "misogi/rule/rails"
28
+ autoload :RSpec, "misogi/rule/rspec"
29
+ end
30
+ end
data/sig/misogi.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Misogi
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: misogi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - iyuuya
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: ファイル内で定義されているクラスやモジュールの名前空間と、実際のファイルパスが一致しているかを検証するlintツールです。
13
+ email:
14
+ - yuya.ito@kufu.co.jp
15
+ executables:
16
+ - misogi
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".editorconfig"
21
+ - ".misogi.yml.example"
22
+ - CHANGELOG.md
23
+ - CLAUDE.md
24
+ - CODE_OF_CONDUCT.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - exe/misogi
29
+ - lib/misogi.rb
30
+ - lib/misogi/cli.rb
31
+ - lib/misogi/configuration.rb
32
+ - lib/misogi/parsed_content.rb
33
+ - lib/misogi/parser/base.rb
34
+ - lib/misogi/parser/ruby.rb
35
+ - lib/misogi/rule/base.rb
36
+ - lib/misogi/rule/rails.rb
37
+ - lib/misogi/rule/rspec.rb
38
+ - lib/misogi/rule/ruby_standard.rb
39
+ - lib/misogi/validator.rb
40
+ - lib/misogi/version.rb
41
+ - lib/misogi/violation.rb
42
+ - sig/misogi.rbs
43
+ homepage: https://github.com/kufu-ai/misogi
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/kufu-ai/misogi
48
+ source_code_uri: https://github.com/kufu-ai/misogi
49
+ changelog_uri: https://github.com/kufu-ai/misogi/blob/main/CHANGELOG.md
50
+ rubygems_mfa_required: 'true'
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.2.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.7.2
66
+ specification_version: 4
67
+ summary: ファイルの内容とパスの整合性をチェックするlintツール
68
+ test_files: []