bitclust-core 1.3.0 → 1.6.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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +48 -4
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/search_index_generator.rb
4
+ #
5
+ # Builds a client-side search index compatible with RDoc's Aliki theme.
6
+ #
7
+ # The index is a flat array of entries:
8
+ #
9
+ # { name:, full_name:, type:, path: }
10
+ #
11
+ # Singleton methods and module functions (full_name holding a literal "."/
12
+ # ".#"/"?.", e.g. "File.open", "Kernel.#open", "Kernel?.open" -- see #250)
13
+ # additionally carry a +match_name+: full_name with "?." folded to ".#" and
14
+ # every "." turned into "::". This mirrors the "." -> "::" rewrite the
15
+ # vendored Aliki ranker's parseQuery() applies to the *query* text (RDoc's
16
+ # own full_names use "::" for class methods), so search_init.js/search_page.js
17
+ # can feature-detect-wrap computeScore() to compare like-for-like without
18
+ # ever touching the displayed full_name. See bitclust#279.
19
+ #
20
+ # { name:, full_name:, type:, path:, match_name: }
21
+ #
22
+ # serialized as a JavaScript assignment so it can be loaded over file://
23
+ # without tripping the browser's CORS check on JSON files:
24
+ #
25
+ # var search_data = { "index": [ ... ] };
26
+ #
27
+ # This mirrors the static layout produced by StatichtmlCommand so that the
28
+ # +path+ of each entry points at the file actually generated there.
29
+ #
30
+
31
+ require 'json'
32
+ require 'bitclust/nameutils'
33
+
34
+ module BitClust
35
+ class SearchIndexGenerator
36
+ include NameUtils
37
+
38
+ # Maps BitClust method typenames onto the type vocabulary understood by
39
+ # Aliki's searcher (class/module/constant/class_method/instance_method).
40
+ # Module functions are callable as singleton methods, so they are indexed
41
+ # as class methods; special variables have no Aliki equivalent and keep
42
+ # their own label (the searcher only uses the type for ranking).
43
+ TYPENAME_TO_SEARCH_TYPE = {
44
+ singleton_method: 'class_method',
45
+ module_function: 'class_method',
46
+ instance_method: 'instance_method',
47
+ constant: 'constant',
48
+ special_variable: 'variable',
49
+ }.freeze
50
+
51
+ # Doc pages (manual/doc/**/*.md) are prose, so language keywords such as
52
+ # "defined?", "undef" or "alias" (see rurema/doctree#2352) are never
53
+ # indexed as a class/method/library. They do, however, live at their own
54
+ # {#id}-anchored heading within a doc page (e.g. spec/def.md's "defined?"
55
+ # section). Rather than hardcoding a keyword list, scan every doc page for
56
+ # ATX headings carrying an explicit {#id} anchor -- the same anchors
57
+ # [ref:d:...] cross references already rely on -- and index each one
58
+ # individually so a keyword search can jump straight to its section.
59
+ # Headings without an explicit anchor aren't linkable, so they are
60
+ # skipped; a "# comment" that happens to open a line inside a fenced code
61
+ # block is not a heading at all, so fenced regions are skipped wholesale.
62
+ ANCHORED_HEADING_RE = /\A(\#{1,6})[ \t]+(.*?)(?:[ \t]+\{#([\w-]+)\})?[ \t]*\z/.freeze
63
+ FENCE_START_RE = /\A`{3,}/.freeze
64
+
65
+ def initialize(suffix: '.html', fs_casesensitive: false)
66
+ @suffix = suffix
67
+ @fs_casesensitive = fs_casesensitive
68
+ end
69
+
70
+ # Builds the search index as an array of entry hashes.
71
+ # +db+ is a MethodDatabase; +fdb+ is an optional FunctionDatabase (C API).
72
+ def build_index(db, fdb = nil)
73
+ index = [] #: Array[entry]
74
+ index.concat(class_entries(db))
75
+ index.concat(method_entries(db))
76
+ index.concat(library_entries(db))
77
+ index.concat(document_entries(db))
78
+ index.concat(document_heading_entries(db))
79
+ index.concat(function_entries(fdb)) if fdb
80
+ index
81
+ end
82
+
83
+ # Serializes the index as an Aliki-compatible +search_data.js+ body.
84
+ def to_js(db, fdb = nil)
85
+ "var search_data = #{JSON.generate(index: build_index(db, fdb))};"
86
+ end
87
+
88
+ # Merges per-version indexes (an array of [version, index] pairs) into a
89
+ # single index whose entries carry a +versions+ array, for the standalone
90
+ # cross-version search page. Entries are considered the same when all of
91
+ # name/full_name/type/path match. Versions are sorted numerically ("3.10"
92
+ # sorts after "3.4"), and entry order is the first appearance while
93
+ # scanning versions in ascending order — independent of the caller's
94
+ # argument order, so the generated file diffs stay stable.
95
+ #
96
+ # A module function's full_name is spelled ".#" by pre-4.0 databases and
97
+ # "?." by 4.0+ ones (bitclust#250), which would split it into two rows —
98
+ # confusing on a page that lists every version side by side. Fold the
99
+ # display to the current "?." spelling here, so both spellings collapse
100
+ # into one row with a unified versions list (name/type/path/match_name
101
+ # are spelling-independent already). Only entries carrying a match_name
102
+ # (dot-qualified methods) are folded: a prose heading whose *text*
103
+ # happens to contain ".#" must keep matching its page verbatim. The
104
+ # single-version in-page search keeps each version's own spelling —
105
+ # build_index is not affected. See the report in bitclust#279.
106
+ def self.merge(version_indexes)
107
+ merged = {} #: Hash[Array[String?], merged_entry]
108
+ sorted = version_indexes.sort_by { |version, _| Gem::Version.new(version) }
109
+ sorted.each do |version, index|
110
+ index.each do |e|
111
+ if e[:match_name] && e[:full_name].include?('.#')
112
+ e = e.merge(full_name: e[:full_name].gsub('.#', '?.'))
113
+ end
114
+ key = e.values_at(:name, :full_name, :type, :path)
115
+ entry = (merged[key] ||= e.merge(versions: []))
116
+ entry[:versions] << version
117
+ end
118
+ end
119
+ merged.values
120
+ end
121
+
122
+ # Serializes a merged multi-version index as a +search_data.js+ body.
123
+ def self.merged_js(version_indexes)
124
+ "var search_data = #{JSON.generate(index: merge(version_indexes))};"
125
+ end
126
+
127
+ private
128
+
129
+ def class_entries(db)
130
+ db.classes.sort.reject(&:dummy?).map do |c|
131
+ {
132
+ name: c.name,
133
+ full_name: c.name,
134
+ type: c.type.to_s,
135
+ path: "class/#{encode(c.name)}#{@suffix}",
136
+ }
137
+ end
138
+ end
139
+
140
+ def method_entries(db)
141
+ seen = {} #: Hash[String, bool]
142
+ result = [] #: Array[entry]
143
+ db.methods.each do |entry|
144
+ next if entry.undefined?
145
+
146
+ type = TYPENAME_TO_SEARCH_TYPE[entry.typename]
147
+ next unless type
148
+
149
+ cname = entry.klass.name
150
+ # bitclust#250: 検索結果に出る表示ラベル(name/full_name)は 4.0
151
+ # 以降のドキュメントでは module function を "?." で表示する。path
152
+ # は識別子なので typechar(常に不変)のまま
153
+ tmark = entry.display_typemark
154
+ tchar = entry.typechar
155
+ entry.names.each do |mname|
156
+ path = "method/#{encode(cname)}/#{tchar}/#{encode(mname)}#{@suffix}"
157
+ next if seen[path]
158
+
159
+ seen[path] = true
160
+ if tmark == '$'
161
+ # A special variable has no owning class to qualify it; its "$"
162
+ # sigil is the only thing distinguishing it, so keep it in +name+
163
+ # too (not just +full_name+) or a "$;"-style query can't match it.
164
+ name = full_name = tmark + mname
165
+ result << { name: name, full_name: full_name, type: type, path: path }
166
+ else
167
+ name = mname
168
+ full_name = cname + tmark + mname
169
+ item = { name: name, full_name: full_name, type: type, path: path } #: entry
170
+ if tmark.include?('.')
171
+ # bitclust#279: singleton methods (".") and module functions
172
+ # (".#"/"?.") keep a literal "." in full_name for display, but
173
+ # the vendored ranker's parseQuery() rewrites every "." in the
174
+ # *query* to "::". match_name is the same rewrite applied to
175
+ # this entry, so the search_init.js/search_page.js computeScore
176
+ # wrap can compare like-for-like. See the file header comment.
177
+ item[:match_name] = full_name.gsub('?.', '.#').gsub('.', '::')
178
+ end
179
+ result << item
180
+ end
181
+ end
182
+ end
183
+ result
184
+ end
185
+
186
+ def library_entries(db)
187
+ db.libraries.sort.map do |lib|
188
+ {
189
+ name: lib.name,
190
+ full_name: lib.name,
191
+ type: 'library',
192
+ path: "library/#{encode(lib.name)}#{@suffix}",
193
+ }
194
+ end
195
+ end
196
+
197
+ def document_entries(db)
198
+ db.docs.map do |doc|
199
+ slug = doc.name
200
+ title = doc.title
201
+ # Prose pages are identified by a Japanese title, so index the title
202
+ # (for human queries) together with the slug (for identifier queries).
203
+ label = (title && !title.empty? && title != slug) ? "#{title} (#{slug})" : slug
204
+ {
205
+ name: label,
206
+ full_name: label,
207
+ type: 'document',
208
+ path: "doc/#{encode(slug)}#{@suffix}",
209
+ }
210
+ end
211
+ end
212
+
213
+ # One entry per {#id}-anchored heading found in a doc page's body, so
214
+ # that e.g. "defined?" or "alias" (headings inside spec/def.md) surface
215
+ # directly instead of only the whole "クラス/メソッドの定義" page. See
216
+ # ANCHORED_HEADING_RE above for why this only looks at doc pages (prose,
217
+ # not method/class references) and only at explicitly anchored headings.
218
+ def document_heading_entries(db)
219
+ seen = {} #: Hash[String, bool]
220
+ result = [] #: Array[entry]
221
+ db.docs.each do |doc|
222
+ source = doc.source
223
+ next unless source
224
+ page_title = (doc.title && !doc.title.empty?) ? doc.title : doc.name
225
+ base_path = "doc/#{encode(doc.name)}#{@suffix}"
226
+ each_anchored_heading(source) do |label, id|
227
+ path = "#{base_path}##{id}"
228
+ next if seen[path]
229
+
230
+ seen[path] = true
231
+ result << {
232
+ name: label,
233
+ full_name: "#{label} (#{page_title})",
234
+ type: 'heading',
235
+ path: path,
236
+ }
237
+ end
238
+ end
239
+ result
240
+ end
241
+
242
+ # Yields [label, id] for each ATX heading in +source+ that carries an
243
+ # explicit {#id} anchor, skipping the contents of fenced code blocks
244
+ # (```lang ... ```) so an in-sample "# comment" line can never be
245
+ # mistaken for a heading.
246
+ def each_anchored_heading(source)
247
+ fence = nil #: Integer?
248
+ source.each_line do |raw|
249
+ line = raw.chomp
250
+ if fence
251
+ # Matches MDCompiler#code_fence's own terminator: the closing fence
252
+ # must be exactly as long as the opening one.
253
+ fence = nil if /\A`{#{fence}}[ \t]*\z/ =~ line
254
+ next
255
+ end
256
+ if m = FENCE_START_RE.match(line)
257
+ fence = (m[0] || raise).size
258
+ next
259
+ end
260
+ next unless m = ANCHORED_HEADING_RE.match(line)
261
+
262
+ id = m[3] or next
263
+ yield clean_heading_label(m[2] || raise), id
264
+ end
265
+ end
266
+
267
+ # Headings may carry inline Markdown (code spans, escaped punctuation);
268
+ # strip that down to plain text for the index entry.
269
+ def clean_heading_label(label)
270
+ label.gsub(/`([^`]*)`/, '\1').gsub(/\\(.)/, '\1')
271
+ end
272
+
273
+ def function_entries(fdb)
274
+ fdb.functions.sort.map do |func|
275
+ {
276
+ name: func.name,
277
+ full_name: func.name,
278
+ type: 'function',
279
+ path: "function/#{func.name}#{@suffix}",
280
+ }
281
+ end
282
+ end
283
+
284
+ def encode(str)
285
+ @fs_casesensitive ? encodename_url(str) : encodename_fs(str)
286
+ end
287
+ end
288
+ end
@@ -40,8 +40,18 @@ module BitClust
40
40
  parser.banner = "Usage: #{@name} <pattern>"
41
41
  unless cmd == 'bitclust'
42
42
  parser.on('-d', '--database=URL', "Database location (default: #{dblocation_name()})") {|loc|
43
- url = (/:/ =~ loc) ? loc : "file://#{File.expand_path(loc)}"
44
- @dblocation = URI.parse(url)
43
+ @dblocation =
44
+ if windows_drive_path?(loc)
45
+ # "G:/Users/foo/.bitclust/db-2.2.0" のような Windows の
46
+ # ドライブレター付きパスは "/:/ =~ loc" が真になってしまい
47
+ # (":" がドライブレターの区切りに含まれるため)、既に URL
48
+ # 形式だと誤認されて file:// を付けずに URI.parse に渡って
49
+ # しまう。drive_path_uri で組み立てる
50
+ drive_path_uri(loc)
51
+ else
52
+ url = (/:/ =~ loc) ? loc : "file://#{File.expand_path(loc)}"
53
+ URI.parse(url)
54
+ end
45
55
  }
46
56
  parser.on('--server=URL', 'Spawns BitClust database server and listen URL. Requires --database option with local path.') {|url|
47
57
  require 'bitclust/server' # require here for speed
@@ -92,6 +102,14 @@ module BitClust
92
102
 
93
103
  def exec(argv, options = {})
94
104
  db = nil
105
+ prefix = options[:prefix]
106
+ if prefix
107
+ if options[:capi]
108
+ db = BitClust::FunctionDatabase.new(prefix)
109
+ else
110
+ db = BitClust::MethodDatabase.new(prefix)
111
+ end
112
+ end
95
113
  if @listen_url
96
114
  spawn_server db
97
115
  else
@@ -107,7 +125,7 @@ module BitClust
107
125
 
108
126
  def server_mode_check(argv)
109
127
  if @dblocation
110
- unless @dblocation.scheme == 'file'
128
+ unless @dblocation&.scheme == 'file'
111
129
  $stderr.puts "Give local path to --database option on server mode"
112
130
  exit 1
113
131
  end
@@ -140,19 +158,19 @@ module BitClust
140
158
  #compiler = RDCompiler::Text.new
141
159
  compiler = Plain.new
142
160
  @view = TerminalView.new(compiler,
143
- {:describe_all => @describe_all,
161
+ {:describe_all => @describe_all,
144
162
  :line => @linep,
145
163
  :encoding => @encoding})
146
164
  end
147
165
 
148
166
  def spawn_server(db)
149
- Server.new(new_local_database(db)).listen @listen_url, @foreground
167
+ Server.new(new_local_database(db)).listen (@listen_url || raise), @foreground
150
168
  end
151
169
 
152
170
  def new_local_database(db)
153
171
  return db if db
154
- path = @dblocation ? @dblocation.path : dbpath()
155
- MethodDatabase.new(path)
172
+ path = @dblocation ? @dblocation&.path : dbpath()
173
+ MethodDatabase.new(path || raise)
156
174
  end
157
175
 
158
176
  def new_database
@@ -172,15 +190,38 @@ module BitClust
172
190
 
173
191
  def find_dblocation
174
192
  %w( REFE2_SERVER BITCLUST_SERVER ).each do |key|
175
- return URI.parse(ENV[key]) if ENV[key]
193
+ return URI.parse(ENV.fetch(key)) if ENV[key]
176
194
  end
177
195
  if path = dbpath()
178
- URI.parse("file://#{path}")
196
+ # Windows で "G:/Users/foo/.bitclust/db-2.2.0" のようなドライブ
197
+ # レター付きの絶対パスを URI.parse("file://#{path}") に渡すと、
198
+ # "G" が URI のホスト部分として解釈されてしまい、path からドライブ
199
+ # レターが失われる(URI.parse("file://G:/foo").path は "/foo" に
200
+ # なってしまう)。ドライブレター付きパスのときは URI 化せず、
201
+ # drive_path_uri でドライブレターを保持したまま file: URI を作る
202
+ windows_drive_path?(path) ? drive_path_uri(path) : URI.parse("file://#{path}")
179
203
  else
180
204
  nil
181
205
  end
182
206
  end
183
207
 
208
+ # "G:/foo" や "C:\\foo" のような、Windows のドライブレターで始まる
209
+ # 絶対パスかどうかを判定する
210
+ def windows_drive_path?(path)
211
+ /\A[A-Za-z]:/.match?(path)
212
+ end
213
+
214
+ # ドライブレター付きの絶対パスを、ドライブレターを失わずに保持した
215
+ # ままの file: URI (URI::File) にする。通常の URI.parse("file://...")
216
+ # はホスト部分を認識してしまい先頭のドライブレターを取りこぼすため、
217
+ # ここでは URI::Generic#initialize の arg_check を無効にして、
218
+ # "先頭は / から始まる絶対パスであること" という通常の URI のパス
219
+ # 検証を素通りさせ、path にドライブレターを含む文字列をそのまま保持
220
+ # させる
221
+ def drive_path_uri(path)
222
+ URI::File.new('file', nil, '', nil, nil, path, nil, nil, nil, nil, false)
223
+ end
224
+
184
225
  def dbpath
185
226
  env_dbpath() || default_dbpath()
186
227
  end
@@ -188,7 +229,7 @@ module BitClust
188
229
  def env_dbpath
189
230
  [ 'REFE2_DATADIR', 'BITCLUST_DATADIR' ].each do |key|
190
231
  if ENV.key?(key)
191
- unless MethodDatabase.datadir?(ENV[key])
232
+ unless MethodDatabase.datadir?(ENV.fetch(key))
192
233
  raise InvalidDatabase, "environment variable #{key} given but #{ENV[key]} is not a valid BitClust database"
193
234
  end
194
235
  return ENV[key]
@@ -202,7 +243,7 @@ module BitClust
202
243
  [ "#{datadir}/refe2", "#{datadir}/bitclust" ].each do |path|
203
244
  return path if MethodDatabase.datadir?(path)
204
245
  end
205
- config_path = Pathname(ENV['HOME']) + ".bitclust" + "config"
246
+ config_path = Pathname(ENV.fetch('HOME')) + ".bitclust" + "config"
206
247
  if config_path.exist?
207
248
  config = YAML.load_file(config_path)
208
249
  return "#{config[:database_prefix]}-#{config[:default_version]}"
@@ -213,10 +254,14 @@ module BitClust
213
254
  def search_pattern(db, argv)
214
255
  db ||= new_database()
215
256
  @view.database ||= db if @view
257
+ # FIXME なぜか else 節にきかないのでここに書いておく
258
+ # @type var db: MethodDatabase
216
259
  case @target_type || db
217
260
  when :class
261
+ # @type var db: MethodDatabase
218
262
  find_class db, argv[0]
219
263
  when FunctionDatabase
264
+ # @type var db: FunctionDatabase
220
265
  case argv.size
221
266
  when 0
222
267
  show_all_functions db
@@ -226,6 +271,7 @@ module BitClust
226
271
  raise "must not happen: #{argv.size}"
227
272
  end
228
273
  else
274
+ # @type var db: MethodDatabase
229
275
  case argv.size
230
276
  when 0
231
277
  show_all_classes db
@@ -236,7 +282,7 @@ module BitClust
236
282
  find_method db, c, nil, m
237
283
  when 3
238
284
  c, t, m = *argv
239
- check_method_type t
285
+ check_method_type t || raise
240
286
  find_method db, c, t, m
241
287
  else
242
288
  raise "must not happen: #{argv.size}"
@@ -278,7 +324,8 @@ module BitClust
278
324
  when /\A\$/ # Special variable.
279
325
  find_method db, 'Kernel', '$', pattern.sub(/\A\$/, '')
280
326
  when /[\#,]\.|\.[\#,]|[\#\.\,]/ # method spec
281
- find_method db, *parse_method_spec_pattern(pattern)
327
+ c, t, m = parse_method_spec_pattern(pattern)
328
+ find_method db, c, t, m
282
329
  when /::/ # Class name or constant name.
283
330
  find_constant db, pattern
284
331
  when /\A[A-Z]/ # Method name or class name, but class name is better.
@@ -305,11 +352,13 @@ module BitClust
305
352
  find_method db, cnames.join('::'), '::', name
306
353
  end
307
354
 
355
+ # 区切りは反転文字列上で探す。"?." は 4.0 以降の module function の
356
+ # 表示形式(反転すると ".?")で、内部表記の ".#" に正規化する
308
357
  def parse_method_spec_pattern(pat)
309
- _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|[\#\.\,])/, 2)
310
- c = _c.reverse
311
- t = _t.tr(',', '#').sub(/\#\./, '.#')
312
- m = _m.reverse
358
+ _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|\.\?|[\#\.\,])/, 2)
359
+ c = (_c || raise).reverse
360
+ t = (_t || raise).tr(',', '#').sub(/\#\.|\.\?/, '.#')
361
+ m = (_m || raise).reverse
313
362
  return c, t, m
314
363
  end
315
364
 
@@ -465,7 +514,7 @@ module BitClust
465
514
  puts title
466
515
  print_names methods.map{|m| m.name }
467
516
  end
468
-
517
+
469
518
  def describe_method(rec)
470
519
  unless rec.entry.library.name == '_builtin'
471
520
  puts "require '#{rec.entry.library.name}'"
@@ -501,10 +550,10 @@ module BitClust
501
550
  end
502
551
 
503
552
  def convert(string)
504
- return string if @database.nil?
553
+ return string if database.nil?
505
554
  _output_encoding = output_encoding
506
555
  return string if _output_encoding.nil?
507
- input_nkf_option = encoding_to_nkf_option(@database.encoding)
556
+ input_nkf_option = encoding_to_nkf_option(database.encoding)
508
557
  output_nkf_option = encoding_to_nkf_option(_output_encoding)
509
558
  if input_nkf_option and output_nkf_option
510
559
  NKF.nkf("-#{input_nkf_option.upcase}#{output_nkf_option}", string)
@@ -9,12 +9,14 @@ module BitClust
9
9
  include NameUtils
10
10
 
11
11
  module_function
12
-
12
+
13
13
  def search_pattern(db, pat)
14
14
  pat = to_pattern(pat)
15
15
  return [] if pat.empty? or /\A\s+\z/ =~ pat
16
16
  cname, type, mname = parse_method_spec_pattern(pat)
17
- cs = ms = []
17
+ # @type var cs: Array[ClassEntry]
18
+ # @type var ms: Array[MethodEntry]
19
+ cs = []; ms = []
18
20
  if cname and not cname.empty?
19
21
  if mname
20
22
  ms = find_class_method(db, cname, type, mname)
@@ -23,9 +25,9 @@ module BitClust
23
25
  cs = find_class(db, cname)
24
26
  end
25
27
  elsif type == '$'
26
- ms = find_special_vars(db, mname)
28
+ ms = find_special_vars(db, mname || raise)
27
29
  else
28
- ms = find_methods(db, mname)
30
+ ms = find_methods(db, mname || raise)
29
31
  end
30
32
  ms = ms.sort_by{|e| [e.library.name, e.klass.name] }
31
33
  cs = cs.sort_by{|e| [e.library.name] }
@@ -35,9 +37,9 @@ module BitClust
35
37
  def find_class(db, cname)
36
38
  db.classes.find_all{|c| /\b#{Regexp.escape(cname)}\w*\z/ =~ c.name }
37
39
  end
38
-
40
+
39
41
  def find_class_method(db, cname, type, mname)
40
- ret = []
42
+ ret = [] #: Array[MethodEntry]
41
43
  db.classes.each{|c|
42
44
  if /\b#{Regexp.escape(cname)}/ =~ c.name
43
45
  ret += c.methods.find_all{|m|
@@ -62,24 +64,34 @@ module BitClust
62
64
 
63
65
  def to_pattern(pat)
64
66
  pat = pat.to_str
65
- pat = pat[/\A\s*(.*?)\s*\z/, 1]
67
+ pat = pat[/\A\s*(.*?)\s*\z/, 1] || raise
66
68
  end
67
-
69
+
68
70
  def parse_method_spec_pattern(pat)
69
71
  if /\s/ =~ pat
70
72
  return parse_method_spec_pattern0(pat)
71
73
  end
72
74
  return pat, nil, nil if /\A[A-Z]\w*\z/ =~ pat
73
75
  return nil, '$', $1 if /\$(\S*)/ =~ pat
74
- _m, _t, _c = pat.reverse.split(/(::|[\#,]\.|\.[\#,]|[\#\.\,])/, 2)
76
+ # bitclust#250: "?." is an alternate spelling of the module-function
77
+ # separator ".#" (Ruby >=4.0 docs display it that way since #277; see
78
+ # NameUtils.display_typemark). Accept it here regardless of which
79
+ # Ruby version the query is about -- this is query *input*, not
80
+ # display, so both notations are always accepted. "?" is added to the
81
+ # same reversed-pattern alternative that already matches "#." and ",."
82
+ # (a marker character immediately followed by ".", read in the
83
+ # reversed pattern), so "?." matches the same way "#." already does.
84
+ _m, _t, _c = pat.reverse.split(/(::|[\#,]\.|\.[\#,\?]|[\#\.\,])/, 2)
85
+ _m || raise
75
86
  c = _c.reverse if _c
76
- t = _t.tr(',', '#').sub(/\#\./, '.#') if _t
87
+ t = _t.tr(',?', '##').sub(/\#\./, '.#') if _t
77
88
  m = _m.reverse
78
89
  return c, t, m
79
90
  end
80
91
 
81
- def parse_method_spec_pattern0(q)
82
- q = q.scan(/\S+/)[0..1]
92
+ def parse_method_spec_pattern0(pat)
93
+ # @type var q: Array[String]
94
+ q = _ = pat.scan(/\S+/)[0..1]
83
95
  q = q.reverse unless /\A[A-Z]/ =~ q[0]
84
96
  return q[0], nil, q[1]
85
97
  end
@@ -24,6 +24,12 @@ module BitClust
24
24
  @parser.parse! argv
25
25
  end
26
26
 
27
+ # グローバル --database(options[:prefix])を必要とするか。
28
+ # DB パスを自前の引数で受けるサブコマンド(searchpage)は false を返す
29
+ def needs_database?
30
+ true
31
+ end
32
+
27
33
  def help
28
34
  @parser.help
29
35
  end
@@ -57,7 +63,7 @@ module BitClust
57
63
  def align_progress_bar_title(title)
58
64
  size = title.size
59
65
  if size > 14
60
- title[0..13]
66
+ title[0..13] || raise
61
67
  else
62
68
  title + ' ' * (14 - size)
63
69
  end
@@ -45,10 +45,15 @@ Options:
45
45
  }
46
46
  end
47
47
 
48
+ # DB はサブコマンド自身の -d/--database オプションで受ける
49
+ def needs_database?
50
+ false
51
+ end
52
+
48
53
  def exec(argv, options)
49
54
  classname = argv[0]
50
- db = MethodDatabase.new(@prefix)
51
- ruby = get_ruby(@version)
55
+ db = MethodDatabase.new(@prefix || raise)
56
+ ruby = get_ruby(@version) || raise
52
57
  if classname && !@all
53
58
  check_ancestors(db, ruby, @requires, classname)
54
59
  else
@@ -83,6 +88,7 @@ Options:
83
88
  rescue ClassNotFound => ex
84
89
  $stderr.puts ex.backtrace
85
90
  $stderr.puts "class not found in database : #{classname}"
91
+ # @type var b: Array[String]
86
92
  b = []
87
93
  end
88
94
  unless a.to_set == b.to_set
@@ -95,6 +101,7 @@ Options:
95
101
  end
96
102
 
97
103
  def check_all_ancestors(db, ruby, requires)
104
+ # @type var classnames: Array[String]
98
105
  classnames = []
99
106
  requires.each do |lib|
100
107
  classnames.push(*defined_classes(ruby, lib, []))
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/subcommands/checklink_command.rb
4
+ #
5
+ # DB 中の全ソースをコンパイル経路で走査し、[[c:]]/[[m:]]/[[lib:]]/[[d:]]/
6
+ # [[f:]] 参照のリンク切れを報告する:
7
+ #
8
+ # bitclust -d db-3.4 checklink
9
+ # bitclust -d db-3.4 checklink --capi-database=db-3.4-capi
10
+ #
11
+ # リンク切れが1件でもあれば exit 1(CI 組み込み用)。
12
+ # [[f:]](C API 関数)は --capi-database 指定時のみ検証し、
13
+ # 未指定ならスキップ数だけ報告する。
14
+
15
+ require 'bitclust'
16
+ require 'bitclust/subcommand'
17
+ require 'bitclust/link_checker'
18
+
19
+ module BitClust
20
+ module Subcommands
21
+ class ChecklinkCommand < Subcommand
22
+ def initialize
23
+ super
24
+ @capi_prefix = nil
25
+ @parser.banner = "Usage: #{File.basename($0, '.*')} --database=PATH checklink [options]"
26
+ @parser.on('--capi-database=PATH', 'C API (function) database for [[f:]] refs.') {|path|
27
+ @capi_prefix = path
28
+ }
29
+ end
30
+
31
+ def exec(argv, options)
32
+ super
33
+ db = @db
34
+ unless db.is_a?(MethodDatabase)
35
+ error 'checklink is for method databases (do not use --capi; pass --capi-database instead)'
36
+ end
37
+ capi_prefix = @capi_prefix
38
+ fdb = capi_prefix ? BitClust::FunctionDatabase.new(capi_prefix) : nil
39
+ checker = LinkChecker.new(db, function_database: fdb)
40
+ checker.check_all
41
+ # 同一エントリ内の同一参照の繰り返しは1行にまとめる
42
+ checker.findings.map {|f| "#{f.location}: [[#{f.ref}]] #{f.message}" }.uniq.each do |line|
43
+ puts line
44
+ end
45
+ if fdb.nil? && checker.skipped_function_refs > 0
46
+ puts "note: #{checker.skipped_function_refs} [[f:]] refs skipped (no --capi-database)"
47
+ end
48
+ version = @db.properties['version']
49
+ puts "#{checker.broken_count} broken link(s) in #{version ? "db (version #{version})" : 'db'}"
50
+ exit 1 unless checker.broken_count.zero?
51
+ end
52
+ end
53
+ end
54
+ end