Skip to content

Commit f420f49

Browse files
Add documentation support for lib, fun, union, cstruct, external, and type (RFC #11) (#15447)
Co-authored-by: Johannes Müller <[email protected]>
1 parent e8ca60d commit f420f49

File tree

15 files changed

+297
-47
lines changed

15 files changed

+297
-47
lines changed

spec/compiler/crystal/tools/doc/directives_spec.cr

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ describe Crystal::Doc::Generator do
4747
a_def.visibility.should eq("private")
4848
end
4949

50+
it "shows documentation for nested objects if a lib is marked with :showdoc:" do
51+
program = top_level_semantic(<<-CRYSTAL, wants_doc: true).program
52+
# :showdoc:
53+
lib Foo
54+
# docs for `foo`
55+
fun foo
56+
end
57+
CRYSTAL
58+
59+
generator = Doc::Generator.new program, [""]
60+
61+
generator.must_include?(program.types["Foo"]).should be_true
62+
generator.type(program.types["Foo"]).lookup_method("foo").should_not be_nil
63+
end
64+
5065
it "does not include documentation for methods within a :nodoc: namespace" do
5166
program = top_level_semantic(<<-CRYSTAL, wants_doc: true).program
5267
# :nodoc:
@@ -91,6 +106,19 @@ describe Crystal::Doc::Generator do
91106
generator.must_include?(generator.type(program.types["Foo"]).lookup_path("Baz")).should be_false
92107
end
93108

109+
it "does not include documentation for a :showdoc: fun inside a lib not marked with :showdoc:" do
110+
program = top_level_semantic(<<-CRYSTAL, wants_doc: true).program
111+
lib Foo
112+
# :showdoc:
113+
fun foo
114+
end
115+
CRYSTAL
116+
117+
generator = Doc::Generator.new program, [""]
118+
119+
generator.must_include?(program.types["Foo"]).should be_false
120+
end
121+
94122
it "doesn't show a method marked :nodoc: within a :showdoc: namespace" do
95123
program = top_level_semantic(<<-CRYSTAL, wants_doc: true).program
96124
# :showdoc:
@@ -105,5 +133,22 @@ describe Crystal::Doc::Generator do
105133
generator = Doc::Generator.new program, [""]
106134
generator.type(program.types["Foo"]).lookup_method("foo").should be_nil
107135
end
136+
137+
it "doesn't show a fun marked :nodoc: within a :showdoc: lib" do
138+
program = top_level_semantic(<<-CRYSTAL, wants_doc: true).program
139+
# :showdoc:
140+
lib Foo
141+
# :nodoc:
142+
# Some docs for `foo`
143+
fun foo
144+
145+
fun bar
146+
end
147+
CRYSTAL
148+
149+
generator = Doc::Generator.new program, [""]
150+
generator.type(program.types["Foo"]).lookup_method("foo").should be_nil
151+
generator.type(program.types["Foo"]).lookup_method("bar").should_not be_nil
152+
end
108153
end
109154
end

spec/compiler/parser/parser_doc_spec.cr

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe "Parser doc" do
1616
{"alias", "alias Foo = Bar"},
1717
{"annotation", "@[Some]"},
1818
{"private def", "private def foo\nend"},
19+
{"lib def", "lib MyLib\nend"},
1920
].each do |(desc, code)|
2021
it "includes doc for #{desc}" do
2122
parser = Parser.new(%(
@@ -29,6 +30,53 @@ describe "Parser doc" do
2930
end
3031
end
3132

33+
[
34+
{"type def", "type Foo = Bar"},
35+
{"cstruct def", "struct Name\nend"},
36+
{"union def", "union Name\nend"},
37+
{"fun def", "fun name = Name"},
38+
{"external var", "$errno : Int32"},
39+
].each do |(desc, code)|
40+
it "includes doc for #{desc} inside lib def" do
41+
parser = Parser.new(%(
42+
lib MyLib
43+
# This is Foo.
44+
# Use it well.
45+
#{code}
46+
end
47+
))
48+
parser.wants_doc = true
49+
node = parser.parse
50+
node.as(Crystal::LibDef).body.doc.should eq("This is Foo.\nUse it well.")
51+
end
52+
end
53+
54+
it "includes doc for cstruct fields" do
55+
parser = Parser.new(<<-CRYSTAL)
56+
lib MyLib
57+
struct IntOrFloat
58+
# This is Foo.
59+
# Use it well.
60+
some_int : Int32
61+
# This is Foo.
62+
# Use it well.
63+
some_float, other_float : Float64
64+
end
65+
end
66+
CRYSTAL
67+
68+
parser.wants_doc = true
69+
node = parser.parse
70+
node.as(Crystal::LibDef)
71+
.body.as(Crystal::CStructOrUnionDef)
72+
.body.as(Crystal::Expressions)
73+
.expressions.each do |exp|
74+
exp.as(Crystal::TypeDeclaration)
75+
.var.as(Crystal::Var)
76+
.doc.should eq("This is Foo.\nUse it well.")
77+
end
78+
end
79+
3280
it "disables doc parsing inside defs" do
3381
parser = Parser.new(%(
3482
# doc 1

src/compiler/crystal/semantic/ast.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ module Crystal
686686
end
687687

688688
class External < Def
689+
property? external_var : Bool = false
689690
property real_name : String
690691
property! fun_def : FunDef
691692
property call_convention : LLVM::CallConvention?

src/compiler/crystal/semantic/top_level_visitor.cr

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
556556
scope.types[name] = type
557557
end
558558

559+
attach_doc type, node, annotations
560+
559561
node.resolved_type = type
560562

561563
type.private = true if node.visibility.private?
@@ -626,9 +628,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
626628
type.extern = true
627629
type.extern_union = node.union?
628630

629-
if location = node.location
630-
type.add_location(location)
631-
end
631+
attach_doc type, node, annotations
632632

633633
current_type.types[node.name] = type
634634
end
@@ -641,15 +641,22 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
641641
end
642642

643643
def visit(node : TypeDef)
644+
annotations = read_annotations
644645
type = current_type.types[node.name]?
646+
645647
if type
646648
node.raise "#{node.name} is already defined"
647649
else
648650
typed_def_type = lookup_type(node.type_spec)
649651
typed_def_type = check_allowed_in_lib node.type_spec, typed_def_type
650-
current_type.types[node.name] = TypeDefType.new @program, current_type, node.name, typed_def_type
651-
false
652+
type = TypeDefType.new @program, current_type, node.name, typed_def_type
653+
654+
attach_doc type, node, annotations
655+
656+
current_type.types[node.name] = type
652657
end
658+
659+
false
653660
end
654661

655662
def visit(node : EnumDef)
@@ -1007,6 +1014,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
10071014
external.call_convention = call_convention
10081015
external.varargs = node.varargs?
10091016
external.fun_def = node
1017+
external.return_type = node.return_type
10101018
node.external = external
10111019

10121020
current_type.add_def(external)

src/compiler/crystal/semantic/type_declaration_visitor.cr

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,27 @@ class Crystal::TypeDeclarationVisitor < Crystal::SemanticVisitor
9999
var_type = check_allowed_in_lib node.type_spec, var_type
100100

101101
type = current_type.as(LibType)
102-
type.add_var node.name, var_type, (node.real_name || node.name), thread_local
102+
103+
setter = External.new(
104+
"#{node.name}=", [Arg.new("value", type: var_type)],
105+
Primitive.new("external_var_set", var_type), node.real_name || node.name
106+
).at(node.location)
107+
setter.set_type(var_type)
108+
setter.external_var = true
109+
setter.thread_local = thread_local
110+
setter.doc = node.doc || @annotations.try(&.first?).try(&.doc)
111+
112+
getter = External.new(
113+
"#{node.name}", [] of Arg,
114+
Primitive.new("external_var_get", var_type), node.real_name || node.name
115+
).at(node.location)
116+
getter.set_type(var_type)
117+
getter.external_var = true
118+
getter.thread_local = thread_local
119+
getter.doc = node.doc || @annotations.try(&.first?).try(&.doc)
120+
121+
type.add_def setter
122+
type.add_def getter
103123

104124
false
105125
end
@@ -186,15 +206,25 @@ class Crystal::TypeDeclarationVisitor < Crystal::SemanticVisitor
186206
if type.lookup_instance_var?(var_name)
187207
node.raise "#{type.type_desc} #{type} already defines a field named '#{field_name}'"
188208
end
209+
189210
ivar = MetaTypeVar.new(var_name, field_type)
211+
ivar.doc = node.var.as(Var).doc
190212
ivar.owner = type
213+
191214
declare_c_struct_or_union_field(type, field_name, ivar, node.location)
192215
end
193216

194217
def declare_c_struct_or_union_field(type, field_name, var, location)
195218
type.instance_vars[var.name] = var
196-
type.add_def Def.new("#{field_name}=", [Arg.new("value")], Primitive.new("struct_or_union_set").at(location))
197-
type.add_def Def.new(field_name, body: InstanceVar.new(var.name))
219+
220+
setter = Def.new("#{field_name}=", [Arg.new("value")], Primitive.new("struct_or_union_set").at(location)).at(location)
221+
setter.doc = var.doc
222+
223+
getter = Def.new(field_name, body: InstanceVar.new(var.name)).at(location)
224+
getter.doc = var.doc
225+
226+
type.add_def setter
227+
type.add_def getter
198228
end
199229

200230
def declare_instance_var(node, var)

src/compiler/crystal/syntax/ast.cr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ module Crystal
575575
include SpecialVar
576576

577577
property name : String
578+
property doc : String?
578579

579580
def initialize(@name : String)
580581
end
@@ -1630,6 +1631,7 @@ module Crystal
16301631
end
16311632

16321633
class TypeDeclaration < ASTNode
1634+
property doc : String?
16331635
property var : ASTNode
16341636
property declared_type : ASTNode
16351637
property value : ASTNode?
@@ -1925,6 +1927,7 @@ module Crystal
19251927

19261928
class LibDef < ASTNode
19271929
property name : Path
1930+
property doc : String?
19281931
property body : ASTNode
19291932
property name_location : Location?
19301933
property visibility = Visibility::Public
@@ -1980,6 +1983,7 @@ module Crystal
19801983

19811984
class TypeDef < ASTNode
19821985
property name : String
1986+
property doc : String?
19831987
property type_spec : ASTNode
19841988
property name_location : Location?
19851989

@@ -2002,6 +2006,7 @@ module Crystal
20022006
# A c struct/union definition inside a lib declaration
20032007
class CStructOrUnionDef < ASTNode
20042008
property name : String
2009+
property doc : String?
20052010
property body : ASTNode
20062011
property? union : Bool
20072012

@@ -2044,6 +2049,7 @@ module Crystal
20442049

20452050
class ExternalVar < ASTNode
20462051
property name : String
2052+
property doc : String?
20472053
property type_spec : ASTNode
20482054
property real_name : String?
20492055

src/compiler/crystal/syntax/parser.cr

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5637,6 +5637,7 @@ module Crystal
56375637
end
56385638

56395639
def parse_lib
5640+
doc = @token.doc
56405641
location = @token.location
56415642
next_token_skip_space_or_newline
56425643

@@ -5652,6 +5653,7 @@ module Crystal
56525653

56535654
lib_def = LibDef.new(name, body).at(location).at_end(end_location)
56545655
lib_def.name_location = name_location
5656+
lib_def.doc = doc
56555657
lib_def
56565658
end
56575659

@@ -5708,6 +5710,7 @@ module Crystal
57085710
skip_statement_end
57095711
Assign.new(ident, value)
57105712
when .global?
5713+
doc = @token.doc
57115714
location = @token.location
57125715
name = @token.value.to_s[1..-1]
57135716
next_token_skip_space_or_newline
@@ -5727,6 +5730,7 @@ module Crystal
57275730

57285731
skip_statement_end
57295732
ExternalVar.new(name, type, real_name)
5733+
.at(location).tap(&.doc=(doc))
57305734
when .op_lcurly_lcurly?
57315735
parse_percent_macro_expression
57325736
when .op_lcurly_percent?
@@ -5964,6 +5968,7 @@ module Crystal
59645968
end
59655969

59665970
def parse_type_def
5971+
doc = @token.doc
59675972
next_token_skip_space_or_newline
59685973
name = check_const
59695974
name_location = @token.location
@@ -5976,10 +5981,13 @@ module Crystal
59765981

59775982
typedef = TypeDef.new name, type
59785983
typedef.name_location = name_location
5984+
typedef.doc = doc
5985+
59795986
typedef
59805987
end
59815988

59825989
def parse_c_struct_or_union(union : Bool)
5990+
doc = @token.doc
59835991
location = @token.location
59845992
next_token_skip_space_or_newline
59855993
name = check_const
@@ -5989,7 +5997,9 @@ module Crystal
59895997
end_location = token_end_location
59905998
next_token_skip_space
59915999

5992-
CStructOrUnionDef.new(name, Expressions.from(body), union: union).at(location).at_end(end_location)
6000+
CStructOrUnionDef.new(name, Expressions.from(body), union: union)
6001+
.at(location).at_end(end_location)
6002+
.tap(&.doc=(doc))
59936003
end
59946004

59956005
def parse_c_struct_or_union_body
@@ -6031,6 +6041,7 @@ module Crystal
60316041
end
60326042

60336043
def parse_c_struct_or_union_fields(exps)
6044+
doc = @token.doc
60346045
vars = [Var.new(@token.value.to_s).at(@token.location).at_end(token_end_location)]
60356046

60366047
next_token_skip_space_or_newline
@@ -6049,6 +6060,7 @@ module Crystal
60496060
skip_statement_end
60506061

60516062
vars.each do |var|
6063+
var.doc = doc
60526064
exps << TypeDeclaration.new(var, type).at(var).at_end(type)
60536065
end
60546066
end

src/compiler/crystal/tools/doc/generator.cr

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,10 @@ class Crystal::Doc::Generator
143143
return false if nodoc? ns
144144
end
145145

146-
# Don't include lib types or types inside a lib type
147-
return false if type.is_a?(Crystal::LibType) || type.namespace.is_a?(LibType)
146+
# Don't include lib types or types inside a lib type unless specified with `:showdoc:`
147+
if (type.is_a?(LibType) || type.namespace.is_a?(LibType)) && !showdoc?(type)
148+
return false
149+
end
148150

149151
!!type.locations.try &.any? do |type_location|
150152
must_include? type_location
@@ -262,7 +264,7 @@ class Crystal::Doc::Generator
262264

263265
parent.types?.try &.each_value do |type|
264266
case type
265-
when Const, LibType
267+
when Const
266268
next
267269
else
268270
types << type(type) if must_include? type

0 commit comments

Comments
 (0)