yard-rustdoc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 79bdb3aec8e1f714e0171aa730ac8f7ae76a2c03a833469e881c509965b2e570
4
+ data.tar.gz: 3b9c82a3687ac3dd58f94932e63f1a71ef390314f7a1940dd2b1f2618aec8845
5
+ SHA512:
6
+ metadata.gz: eff3df27844e46ad1ff348af7149e7e87758249f0478afbd88b68692ba84338a9b1a970091b14f703491b6cf78738ebb61b16dc3eae2bc8c49c450ea9f1a2d7d
7
+ data.tar.gz: be52e56f4bb91b3047b57b8fd1518b9c094e74b0601fdc32349c3e425beac0a6914589997955a9fc37f2a0673c35336cf77dd77b269284578d53092d4f7d26a2
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # YARD::Rustdoc
2
+
3
+ YARD plugin for documenting Magnus-based Rust gems. Supports writing class
4
+ documentation on Struct and method documentation on Struct methods.
5
+
6
+ **Note**: WIP, not released.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'yard-rustdoc'
14
+ ```
15
+
16
+ Load the plugin through `--plugin rustdoc` (e.g. in your project's `.yardopts`).
17
+ See `test/samples/example-ext` for full example.
18
+
19
+ ## Usage
20
+
21
+ 1. Write YARD-compatible documentation in Rust documentation block
22
+ (`///` or `//!`), and tag them with `@yard`.
23
+
24
+ 2. Generate Rustdoc as JSON:
25
+
26
+ ```sh
27
+ cargo +nightly rustdoc -p path/to/extension -- \
28
+ -Zunstable-options --output-format json \
29
+ --document-private-items
30
+ ```
31
+
32
+ `nightly` is required because the JSON format isn't stable yet.
33
+ `--document-private-items` is included so that you don't have to make
34
+ everything in the crate public.
35
+
36
+ 3. Run YARD on Ruby files and the Rustdoc's JSON:
37
+
38
+ ```sh
39
+ yard lib path/to/rustdoc.json
40
+ ```
41
+
42
+ ### Writing documentation
43
+
44
+ YARD::Rustdoc only targets docblocks starting with `@yard` so that you can
45
+ still write Rust-style docblocks for non-Ruby parts of your crate.
46
+
47
+ #### Examples
48
+
49
+ The class name will be extracted from Magnus' `class =`.
50
+
51
+ ```rust
52
+ /// @yard
53
+ /// High-level documentation for Foo::Bar.
54
+ #[magnus(class = "Foo::Bar")]
55
+ pub struct Bar { }
56
+ ```
57
+
58
+ The `@rename` tag renames the class to `Foo::Baz`.
59
+
60
+ ```rust
61
+ /// @yard
62
+ /// @remame Foo::Baz
63
+ pub struct InnerName { }
64
+ ```
65
+
66
+ Defines `Foo::Bar.new` -- class method because the first argument isn't `self`.
67
+
68
+ ```rust
69
+ impl Bar {
70
+ /// @yard
71
+ /// @return [Foo::Bar]
72
+ fn build() -> Self {}
73
+ }
74
+ ```
75
+
76
+ Defines `Foo::Bar#baz`:
77
+
78
+ ```rust
79
+ impl Bar {
80
+ /// @yard
81
+ fn baz(&self) {}
82
+ }
83
+ ```
84
+
85
+ Specifies the method's name and params with `@def`. This lets YARD know which params
86
+ are required, optional, keyword arguments, etc.
87
+
88
+ `@def` must be a single, valid ruby method definition, without the `end`.
89
+
90
+ ```rust
91
+ impl Bar {
92
+ /// @yard
93
+ /// @def qux=(val = "")
94
+ /// @param val [Object]
95
+ fn write_qux(&self, val: Value) {}
96
+ }
97
+ ```
98
+
99
+ This will be ignored as it's not tagged with `@yard`.
100
+
101
+ ```rust
102
+ impl Bar {
103
+ fn secret {}
104
+ }
105
+ ```
106
+
107
+ #### Tips
108
+
109
+ YARD's syntax differs from what Rustdoc expects. Linters you man want to disable:
110
+
111
+ ```rust
112
+ #![allow(rustdoc::broken_intra_doc_links)]
113
+ #![allow(rustdoc::invalid_html_tags)]
114
+ ```
115
+
116
+ ## Development
117
+
118
+ Run tests with `rake`. The tests use a sample project located in
119
+ `test/samples/example-ext`. To regenerate its json doc, run `rake doc:rustdoc`
120
+ from that directory. See the test project's Rakefile for details.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module YARD
6
+ module Rustdoc
7
+ class DefParser
8
+ # The name of the `def`'d method
9
+ attr_reader(:name)
10
+
11
+ # The params of the `def`'d method
12
+ # @return [Array<[String, [String, nil]]>] An array of tuple where 0 is
13
+ # the name of the arg with its specifier (&, **, :) and 1 is the default.
14
+ attr_reader(:parameters)
15
+
16
+ def initialize(string)
17
+ @string = string
18
+ @name = nil
19
+ @parameters = []
20
+ parse!
21
+ end
22
+
23
+ private
24
+
25
+ def parse!
26
+ string = "def #{@string}; end"
27
+ def_node = begin
28
+ SyntaxTree.parse(string) # Program
29
+ .child_nodes.first # Statements
30
+ .child_nodes.first # Def
31
+ rescue => e
32
+ log.debug(e)
33
+ return log.warn("failed to extract def node from '#{@string}'\n #{e.message}")
34
+ end
35
+ @name = def_node.name.value
36
+
37
+ params = def_node.params
38
+ params = params.contents if params.is_a?(SyntaxTree::Paren)
39
+
40
+ params.requireds.each { |name| add_param(name) }
41
+ params.optionals.each { |name, default| add_param(name, default) }
42
+ add_param(params.rest) if params.rest
43
+ params.posts.each { |name| add_param(name) }
44
+ params.keywords.each { |name, default| add_param(name, default) }
45
+ add_param(params.keyword_rest) if params.keyword_rest
46
+
47
+ add_param(params.block) if params.block
48
+ end
49
+
50
+ def format(node)
51
+ formatter = SyntaxTree::Formatter.new(nil)
52
+ node.format(formatter)
53
+ formatter.flush
54
+ formatter.output
55
+ end
56
+
57
+ def add_param(node, default = nil)
58
+ default = format(default) if default
59
+
60
+ @parameters << [format(node), default]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD::Handlers
4
+ module Rustdoc
5
+ class Base < YARD::Handlers::Base
6
+ include YARD::Parser::Rustdoc
7
+
8
+ def self.statement_class(klass = nil)
9
+ @statement_classes ||= []
10
+ @statement_classes << klass
11
+
12
+ nil
13
+ end
14
+
15
+ def self.handles?(statement, processor)
16
+ handles = true
17
+ if @statement_classes.any?
18
+ handles &&= @statement_classes.any? { |klass| statement.is_a?(klass) }
19
+ end
20
+
21
+ handles
22
+ end
23
+
24
+ def register_file_info(object, file = statement.file, line = statement.line, comments = statement.docstring)
25
+ super
26
+ end
27
+
28
+ def register_docstring(object, docstring = statement.docstring, stmt = statement)
29
+ super
30
+ end
31
+ end
32
+
33
+ class StructHandler < Base
34
+ statement_class(Statements::Struct)
35
+
36
+ process do
37
+ obj = YARD::CodeObjects::ClassObject.new(:root, statement.name)
38
+ register(obj)
39
+
40
+ push_state(namespace: obj) do
41
+ parser.process(statement.methods)
42
+ end
43
+ end
44
+ end
45
+
46
+ class MethodHandler < Base
47
+ statement_class(Statements::Method)
48
+
49
+ process do
50
+ obj = MethodObject.new(namespace, statement.name, statement.scope)
51
+ obj.parameters = statement.parameters if statement.parameters
52
+ register(obj)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD::Parser::Rustdoc
4
+ class Parser < YARD::Parser::Base
5
+ # This default constructor does nothing. The subclass is responsible for
6
+ # storing the source contents and filename if they are required.
7
+ # @param [String] source the source contents
8
+ # @param [String] filename the name of the file if from disk
9
+ def initialize(source, filename)
10
+ @source = source
11
+ @rustdoc_json = JSON.parse(@source).fetch("index") do
12
+ raise "Expected `index` top-level key in Rustdoc json format"
13
+ end
14
+ @filename = filename
15
+ @entries = []
16
+ end
17
+
18
+ # Override inspect instead of dumping the file content because it is huge.
19
+ def inspect
20
+ "<#{self.class.name} @filename=#{@filename.inspect}>"
21
+ end
22
+
23
+ # Finds Rust Struct for the current crate marked with @yard and extract all
24
+ # the marked methods.
25
+ # @return [Base] this method should return itself
26
+ def parse
27
+ @entries = []
28
+
29
+ @rustdoc_json.each do |id, entry|
30
+ next unless relevant_entry?(entry)
31
+ next unless entry["kind"] == "struct"
32
+
33
+ methods = entry
34
+ .dig("inner", "impls")
35
+ .flat_map { |impl_id| @rustdoc_json.dig(impl_id, "inner", "items") }
36
+ .filter_map do |method_id|
37
+ method_entry = @rustdoc_json.fetch(method_id)
38
+ next unless relevant_entry?(method_entry)
39
+
40
+ Statements::Method.new(method_entry)
41
+ end
42
+
43
+ @entries << Statements::Struct.new(entry, methods)
44
+ end
45
+
46
+ self
47
+ end
48
+
49
+ def tokenize
50
+ raise "Rustdoc Parser does not tokenize"
51
+ end
52
+
53
+ # This method should be implemented to return a list of semantic tokens
54
+ # representing the source code to be post-processed. Otherwise the method
55
+ # should return nil.
56
+ #
57
+ # @abstract
58
+ # @return [Array] a list of semantic tokens representing the source code
59
+ # to be post-processed
60
+ # @return [nil] if no post-processing should be done
61
+ def enumerator
62
+ @entries
63
+ end
64
+
65
+ private
66
+
67
+ def relevant_entry?(entry)
68
+ return false unless entry["crate_id"].zero?
69
+ return false unless entry["docs"]&.include?("@yard")
70
+
71
+ true
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD::Parser::Rustdoc
4
+ module Statements
5
+ class Base
6
+ def initialize(rustdoc)
7
+ @rustdoc = rustdoc
8
+ @name = nil
9
+ @parameters = []
10
+ end
11
+
12
+ def docstring
13
+ @rustdoc.fetch("docs")
14
+ end
15
+
16
+ def source
17
+ return if file.nil?
18
+
19
+ File.read(file)
20
+ .lines
21
+ .slice(line_index_range)
22
+ .join
23
+ rescue Errno::ENOENT
24
+ log.warn("can't read '#{file}', cwd='#{Dir.pwd}'")
25
+
26
+ ""
27
+ end
28
+
29
+ def line
30
+ line_range.first
31
+ end
32
+
33
+ def line_range
34
+ @rustdoc.dig("span", "begin", 0)...@rustdoc.dig("span", "end", 0)
35
+ end
36
+
37
+ def file
38
+ @rustdoc.dig("span", "filename")
39
+ end
40
+
41
+ def show
42
+ @rustdoc.to_s
43
+ end
44
+
45
+ # Not sure what should go here either
46
+ def comments_hash_flag
47
+ end
48
+
49
+ def comments_range
50
+ end
51
+
52
+ private
53
+
54
+ def line_index_range
55
+ (@rustdoc.dig("span", "begin", 0) - 1)..@rustdoc.dig("span", "end", 0)
56
+ end
57
+ end
58
+
59
+ class Struct < Base
60
+ attr_reader(:methods)
61
+
62
+ def initialize(rustdoc, methods)
63
+ super(rustdoc)
64
+ @methods = methods
65
+ end
66
+
67
+ def name
68
+ return $1.strip if docstring =~ /^@rename\s*(.+)/
69
+
70
+ @rustdoc["attrs"].each do |attr|
71
+ next unless attr.include?("magnus")
72
+
73
+ # Extract class name from magnus attrs that define classes such as:
74
+ # - #[magnus::wrap(class = "ClassName")]
75
+ # - #[magnus(class = "ClassName")]
76
+ return $1.strip if attr =~ /class\s*=\s*"([^"]+)"/
77
+ end
78
+
79
+ # Fallback to the struct's name
80
+ @rustdoc.fetch("name")
81
+ end
82
+ end
83
+
84
+ class Method < Base
85
+ def name
86
+ parse_def!
87
+
88
+ @name || @rustdoc.fetch("name")
89
+ end
90
+
91
+ def scope
92
+ first_arg = @rustdoc.dig("inner", "decl", "inputs", 0, 0)
93
+ if first_arg == "self"
94
+ :instance
95
+ else
96
+ :class
97
+ end
98
+ end
99
+
100
+ # Parses the parameters from the @def annotations in the docstring
101
+ def parameters
102
+ parse_def!
103
+
104
+ @parameters
105
+ end
106
+
107
+ private
108
+
109
+ # Extract @def tag from the docstring. Has to be done before we create
110
+ # the CodeObject in the `handler` because:
111
+ # 1. The method name can be overriden -- it's too late for that once
112
+ # the method code object exists.
113
+ # 2. The docstring parser runs automatically after the code object is
114
+ # created, and emits warning on udnefined `@param`. We need to set the
115
+ # method's parameters before then.
116
+ def parse_def!
117
+ return if defined?(@def_parsed)
118
+ @def_parsed = true
119
+
120
+ parsed = YARD::DocstringParser.new.parse(@rustdoc.fetch("docs"))
121
+ def_tag = parsed.tags.find do |tag|
122
+ tag.respond_to?(:tag_name) && tag.tag_name == "def"
123
+ end
124
+
125
+ return unless def_tag
126
+
127
+ parser = YARD::Rustdoc::DefParser.new(def_tag.text)
128
+ @name = parser.name
129
+ @parameters = parser.parameters || []
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YARD
4
+ module Rustdoc
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yard"
5
+
6
+ require_relative "yard-rustdoc/version"
7
+ require_relative "yard-rustdoc/parser"
8
+ require_relative "yard-rustdoc/statements"
9
+ require_relative "yard-rustdoc/handlers"
10
+ require_relative "yard-rustdoc/def_parser"
11
+
12
+ module YARD
13
+ Parser::SourceParser.register_parser_type(:rustdoc, Parser::Rustdoc::Parser, "json")
14
+ Handlers::Processor.register_handler_namespace(:rustdoc, Handlers::Rustdoc)
15
+ Tags::Library.define_tag("Tagging docblock for yard", :yard)
16
+ Tags::Library.define_tag("Renaming class & methods", :rename)
17
+ Tags::Library.define_tag("Specify a method name and args", :def)
18
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yard-rustdoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jimmy Bourassa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yard
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: syntax_tree
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: standard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.9'
83
+ description:
84
+ email:
85
+ - jbourassa@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - README.md
91
+ - lib/yard-rustdoc.rb
92
+ - lib/yard-rustdoc/def_parser.rb
93
+ - lib/yard-rustdoc/handlers.rb
94
+ - lib/yard-rustdoc/parser.rb
95
+ - lib/yard-rustdoc/statements.rb
96
+ - lib/yard-rustdoc/version.rb
97
+ homepage: https://github.com/oxidize-rb/yard-rustdoc
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ homepage_uri: https://github.com/oxidize-rb/yard-rustdoc
102
+ source_code_uri: https://github.com/oxidize-rb/yard-rustdoc
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 2.7.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.3.7
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Generate YARD documentation for Magnus-based Rust gems.
122
+ test_files: []