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.
- checksums.yaml +7 -0
- data/.editorconfig +9 -0
- data/.misogi.yml.example +39 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +191 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +246 -0
- data/Rakefile +12 -0
- data/exe/misogi +6 -0
- data/lib/misogi/cli.rb +248 -0
- data/lib/misogi/configuration.rb +88 -0
- data/lib/misogi/parsed_content.rb +27 -0
- data/lib/misogi/parser/base.rb +22 -0
- data/lib/misogi/parser/ruby.rb +109 -0
- data/lib/misogi/rule/base.rb +33 -0
- data/lib/misogi/rule/rails.rb +165 -0
- data/lib/misogi/rule/rspec.rb +148 -0
- data/lib/misogi/rule/ruby_standard.rb +77 -0
- data/lib/misogi/validator.rb +40 -0
- data/lib/misogi/version.rb +5 -0
- data/lib/misogi/violation.rb +23 -0
- data/lib/misogi.rb +30 -0
- data/sig/misogi.rbs +4 -0
- metadata +68 -0
|
@@ -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,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
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: []
|