From fdd1443cb3c129ba25430dc5b4a4b74eb8d9a6e5 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Sun, 12 Jan 2025 00:26:32 -0500 Subject: [PATCH] Parse docs for lib objects Allow showing docs using :showdoc: directive --- spec/compiler/parser/parser_doc_spec.cr | 48 ++++++++++++ src/compiler/crystal/semantic/ast.cr | 1 + .../crystal/semantic/top_level_visitor.cr | 18 +++-- .../semantic/type_declaration_visitor.cr | 36 ++++++++- src/compiler/crystal/syntax/ast.cr | 6 ++ src/compiler/crystal/syntax/parser.cr | 14 +++- src/compiler/crystal/tools/doc/generator.cr | 18 ++--- .../tools/doc/html/_method_detail.html | 2 +- .../tools/doc/html/_method_summary.html | 2 +- src/compiler/crystal/tools/doc/html/type.html | 47 ++++++++---- src/compiler/crystal/tools/doc/macro.cr | 3 + src/compiler/crystal/tools/doc/method.cr | 28 ++++++- src/compiler/crystal/tools/doc/type.cr | 76 ++++++++++++++++++- src/compiler/crystal/types.cr | 13 ---- 14 files changed, 255 insertions(+), 57 deletions(-) diff --git a/spec/compiler/parser/parser_doc_spec.cr b/spec/compiler/parser/parser_doc_spec.cr index 659fb3b0d70a..14322436082c 100644 --- a/spec/compiler/parser/parser_doc_spec.cr +++ b/spec/compiler/parser/parser_doc_spec.cr @@ -16,6 +16,7 @@ describe "Parser doc" do {"alias", "alias Foo = Bar"}, {"annotation", "@[Some]"}, {"private def", "private def foo\nend"}, + {"lib def", "lib MyLib\nend"}, ].each do |(desc, code)| it "includes doc for #{desc}" do parser = Parser.new(%( @@ -29,6 +30,53 @@ describe "Parser doc" do end end + [ + {"type def", "type Foo = Bar"}, + {"cstruct def", "struct Name\nend"}, + {"union def", "union Name\nend"}, + {"fun def", "fun name = Name"}, + {"external var", "$errno : Int32"}, + ].each do |(desc, code)| + it "includes doc for #{desc} inside lib def" do + parser = Parser.new(%( + lib MyLib + # This is Foo. + # Use it well. + #{code} + end + )) + parser.wants_doc = true + node = parser.parse + node.as(Crystal::LibDef).body.doc.should eq("This is Foo.\nUse it well.") + end + end + + it "includes doc for cstruct fields" do + parser = Parser.new(<<-CRYSTAL) + lib MyLib + struct IntOrFloat + # This is Foo. + # Use it well. + some_int : Int32 + # This is Foo. + # Use it well. + some_float, other_float : Float64 + end + end + CRYSTAL + + parser.wants_doc = true + node = parser.parse + node.as(Crystal::LibDef) + .body.as(Crystal::CStructOrUnionDef) + .body.as(Crystal::Expressions) + .expressions.each do |exp| + exp.as(Crystal::TypeDeclaration) + .var.as(Crystal::Var) + .doc.should eq("This is Foo.\nUse it well.") + end + end + it "disables doc parsing inside defs" do parser = Parser.new(%( # doc 1 diff --git a/src/compiler/crystal/semantic/ast.cr b/src/compiler/crystal/semantic/ast.cr index f4fa683efe82..2cac47726e72 100644 --- a/src/compiler/crystal/semantic/ast.cr +++ b/src/compiler/crystal/semantic/ast.cr @@ -686,6 +686,7 @@ module Crystal end class External < Def + property? external_var : Bool = false property real_name : String property! fun_def : FunDef property call_convention : LLVM::CallConvention? diff --git a/src/compiler/crystal/semantic/top_level_visitor.cr b/src/compiler/crystal/semantic/top_level_visitor.cr index 3654e24ff7a5..799cfc0d947c 100644 --- a/src/compiler/crystal/semantic/top_level_visitor.cr +++ b/src/compiler/crystal/semantic/top_level_visitor.cr @@ -556,6 +556,8 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor scope.types[name] = type end + attach_doc type, node, annotations + node.resolved_type = type type.private = true if node.visibility.private? @@ -626,9 +628,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor type.extern = true type.extern_union = node.union? - if location = node.location - type.add_location(location) - end + attach_doc type, node, annotations current_type.types[node.name] = type end @@ -641,15 +641,22 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor end def visit(node : TypeDef) + annotations = read_annotations type = current_type.types[node.name]? + if type node.raise "#{node.name} is already defined" else typed_def_type = lookup_type(node.type_spec) typed_def_type = check_allowed_in_lib node.type_spec, typed_def_type - current_type.types[node.name] = TypeDefType.new @program, current_type, node.name, typed_def_type - false + type = TypeDefType.new @program, current_type, node.name, typed_def_type + + attach_doc type, node, annotations + + current_type.types[node.name] = type end + + false end def visit(node : EnumDef) @@ -1000,6 +1007,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor external.call_convention = call_convention external.varargs = node.varargs? external.fun_def = node + external.return_type = node.return_type node.external = external current_type.add_def(external) diff --git a/src/compiler/crystal/semantic/type_declaration_visitor.cr b/src/compiler/crystal/semantic/type_declaration_visitor.cr index bcb3a63e6eb6..d19902ffe4e1 100644 --- a/src/compiler/crystal/semantic/type_declaration_visitor.cr +++ b/src/compiler/crystal/semantic/type_declaration_visitor.cr @@ -99,7 +99,27 @@ class Crystal::TypeDeclarationVisitor < Crystal::SemanticVisitor var_type = check_allowed_in_lib node.type_spec, var_type type = current_type.as(LibType) - type.add_var node.name, var_type, (node.real_name || node.name), thread_local + + setter = External.new( + "#{node.name}=", [Arg.new("value", type: var_type)], + Primitive.new("external_var_set", var_type), node.real_name || node.name + ).at(node.location) + setter.set_type(var_type) + setter.external_var = true + setter.thread_local = thread_local + setter.doc = node.doc || @annotations.try(&.first?).try(&.doc) + + getter = External.new( + "#{node.name}", [] of Arg, + Primitive.new("external_var_get", var_type), node.real_name || node.name + ).at(node.location) + getter.set_type(var_type) + getter.external_var = true + getter.thread_local = thread_local + getter.doc = node.doc || @annotations.try(&.first?).try(&.doc) + + type.add_def setter + type.add_def getter false end @@ -186,15 +206,25 @@ class Crystal::TypeDeclarationVisitor < Crystal::SemanticVisitor if type.lookup_instance_var?(var_name) node.raise "#{type.type_desc} #{type} already defines a field named '#{field_name}'" end + ivar = MetaTypeVar.new(var_name, field_type) + ivar.doc = node.var.as(Var).doc ivar.owner = type + declare_c_struct_or_union_field(type, field_name, ivar, node.location) end def declare_c_struct_or_union_field(type, field_name, var, location) type.instance_vars[var.name] = var - type.add_def Def.new("#{field_name}=", [Arg.new("value")], Primitive.new("struct_or_union_set").at(location)) - type.add_def Def.new(field_name, body: InstanceVar.new(var.name)) + + setter = Def.new("#{field_name}=", [Arg.new("value")], Primitive.new("struct_or_union_set").at(location)).at(location) + setter.doc = var.doc + + getter = Def.new(field_name, body: InstanceVar.new(var.name)).at(location) + getter.doc = var.doc + + type.add_def setter + type.add_def getter end def declare_instance_var(node, var) diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr index fb443e2e6777..042c6ed5a1df 100644 --- a/src/compiler/crystal/syntax/ast.cr +++ b/src/compiler/crystal/syntax/ast.cr @@ -575,6 +575,7 @@ module Crystal include SpecialVar property name : String + property doc : String? def initialize(@name : String) end @@ -1630,6 +1631,7 @@ module Crystal end class TypeDeclaration < ASTNode + property doc : String? property var : ASTNode property declared_type : ASTNode property value : ASTNode? @@ -1925,6 +1927,7 @@ module Crystal class LibDef < ASTNode property name : Path + property doc : String? property body : ASTNode property name_location : Location? property visibility = Visibility::Public @@ -1980,6 +1983,7 @@ module Crystal class TypeDef < ASTNode property name : String + property doc : String? property type_spec : ASTNode property name_location : Location? @@ -2002,6 +2006,7 @@ module Crystal # A c struct/union definition inside a lib declaration class CStructOrUnionDef < ASTNode property name : String + property doc : String? property body : ASTNode property? union : Bool @@ -2044,6 +2049,7 @@ module Crystal class ExternalVar < ASTNode property name : String + property doc : String? property type_spec : ASTNode property real_name : String? diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index 60a3ec6414a7..1547a405b4c7 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -5633,6 +5633,7 @@ module Crystal end def parse_lib + doc = @token.doc location = @token.location next_token_skip_space_or_newline @@ -5648,6 +5649,7 @@ module Crystal lib_def = LibDef.new(name, body).at(location).at_end(end_location) lib_def.name_location = name_location + lib_def.doc = doc lib_def end @@ -5704,6 +5706,7 @@ module Crystal skip_statement_end Assign.new(ident, value) when .global? + doc = @token.doc location = @token.location name = @token.value.to_s[1..-1] next_token_skip_space_or_newline @@ -5723,6 +5726,7 @@ module Crystal skip_statement_end ExternalVar.new(name, type, real_name) + .at(location).tap(&.doc=(doc)) when .op_lcurly_lcurly? parse_percent_macro_expression when .op_lcurly_percent? @@ -5960,6 +5964,7 @@ module Crystal end def parse_type_def + doc = @token.doc next_token_skip_space_or_newline name = check_const name_location = @token.location @@ -5972,10 +5977,13 @@ module Crystal typedef = TypeDef.new name, type typedef.name_location = name_location + typedef.doc = doc + typedef end def parse_c_struct_or_union(union : Bool) + doc = @token.doc location = @token.location next_token_skip_space_or_newline name = check_const @@ -5985,7 +5993,9 @@ module Crystal end_location = token_end_location next_token_skip_space - CStructOrUnionDef.new(name, Expressions.from(body), union: union).at(location).at_end(end_location) + CStructOrUnionDef.new(name, Expressions.from(body), union: union) + .at(location).at_end(end_location) + .tap(&.doc=(doc)) end def parse_c_struct_or_union_body @@ -6027,6 +6037,7 @@ module Crystal end def parse_c_struct_or_union_fields(exps) + doc = @token.doc vars = [Var.new(@token.value.to_s).at(@token.location).at_end(token_end_location)] next_token_skip_space_or_newline @@ -6045,6 +6056,7 @@ module Crystal skip_statement_end vars.each do |var| + var.doc = doc exps << TypeDeclaration.new(var, type).at(var).at_end(type) end end diff --git a/src/compiler/crystal/tools/doc/generator.cr b/src/compiler/crystal/tools/doc/generator.cr index d146101b069e..738cbf4b7f47 100644 --- a/src/compiler/crystal/tools/doc/generator.cr +++ b/src/compiler/crystal/tools/doc/generator.cr @@ -143,8 +143,10 @@ class Crystal::Doc::Generator return false if nodoc? ns end - # Don't include lib types or types inside a lib type - return false if type.is_a?(Crystal::LibType) || type.namespace.is_a?(LibType) + # Don't include lib types or types inside a lib type unless specified with `:showdoc:` + if (type.is_a?(LibType) || type.namespace.is_a?(LibType)) && !showdoc?(type) + return false + end !!type.locations.try &.any? do |type_location| must_include? type_location @@ -234,16 +236,6 @@ class Crystal::Doc::Generator false end - def showdoc?(obj) - return false if !@program.wants_doc? - - if showdoc?(obj.doc.try &.strip) - return true - end - - false - end - def crystal_builtin?(type) return false unless project_info.crystal_stdlib? # TODO: Enabling this allows links to `NoReturn` to work, but has two `NoReturn`s show up in the sidebar @@ -282,7 +274,7 @@ class Crystal::Doc::Generator parent.types?.try &.each_value do |type| case type - when Const, LibType + when Const next else types << type(type) if must_include? type diff --git a/src/compiler/crystal/tools/doc/html/_method_detail.html b/src/compiler/crystal/tools/doc/html/_method_detail.html index 1520d2a4a610..97e46b14b7d7 100644 --- a/src/compiler/crystal/tools/doc/html/_method_detail.html +++ b/src/compiler/crystal/tools/doc/html/_method_detail.html @@ -7,7 +7,7 @@

<%= method.abstract? ? "abstract " : "" %><%= method.visibility %> - <%= method.kind %><%= method.name %><%= method.args_to_html %> + <%= method.kind %><%= method.name %><%= method.real_name %><%= method.args_to_html %> #
diff --git a/src/compiler/crystal/tools/doc/html/_method_summary.html b/src/compiler/crystal/tools/doc/html/_method_summary.html index f59435ea5459..0c8b340d4183 100644 --- a/src/compiler/crystal/tools/doc/html/_method_summary.html +++ b/src/compiler/crystal/tools/doc/html/_method_summary.html @@ -6,7 +6,7 @@

    <% methods.each do |method| %>
  • - <%= method.prefix %><%= method.name %><%= method.args_to_html(:highlight) %> + <%= method.prefix %><%= method.name %><%= method.real_name %><%= method.args_to_html(:highlight) %> <% if summary = method.formatted_summary %>
    <%= summary %>
    <% end %> diff --git a/src/compiler/crystal/tools/doc/html/type.html b/src/compiler/crystal/tools/doc/html/type.html index fb1dbbe94f13..c1ce3a1cd6b6 100644 --- a/src/compiler/crystal/tools/doc/html/type.html +++ b/src/compiler/crystal/tools/doc/html/type.html @@ -47,6 +47,14 @@

    <%= type.formatted_alias_definition %> <% end %> +<% if type.type_def? %> +

    + <%= Crystal::Doc.anchor_link("type-definition") %> + Type Definition +

    + <%= type.formatted_type_definition %> +<% end %> + <%= OtherTypesTemplate.new("Included Modules", type, type.included_modules) %> <%= OtherTypesTemplate.new("Extended Modules", type, type.extended_modules) %> <%= OtherTypesTemplate.new("Direct Known Subclasses", type, type.subclasses) %> @@ -95,24 +103,31 @@

    <% end %> -<%= MethodSummaryTemplate.new("Constructors", type.constructors) %> -<%= MethodSummaryTemplate.new(type.program? ? "Method Summary" : "Class Method Summary", type.class_methods) %> -<%= MethodSummaryTemplate.new("Macro Summary", type.macros) %> -<%= MethodSummaryTemplate.new("Instance Method Summary", type.instance_methods) %> +<% if type.lib? %> + <%= MethodSummaryTemplate.new("External Variable Summary", type.external_vars) %> + <%= MethodSummaryTemplate.new("Function Summary", type.functions) %> + <%= MethodDetailTemplate.new("External Variable Detail", type.external_vars) %> + <%= MethodDetailTemplate.new("Function Detail", type.functions) %> +<% elsif !type.type_def? %> + <%= MethodSummaryTemplate.new("Constructors", type.constructors) %> + <%= MethodSummaryTemplate.new(type.program? ? "Method Summary" : "Class Method Summary", type.class_methods) %> + <%= MethodSummaryTemplate.new("Macro Summary", type.macros) %> + <%= MethodSummaryTemplate.new("Instance Method Summary", type.instance_methods) %> -
    - <% type.ancestors.each do |ancestor| %> - <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.instance_methods, "Instance") %> - <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.constructors, "Constructor") %> - <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.class_methods, "Class") %> - <%= MacrosInheritedTemplate.new(type, ancestor, ancestor.macros) %> - <% end %> -
    +
    + <% type.ancestors.each do |ancestor| %> + <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.instance_methods, "Instance") %> + <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.constructors, "Constructor") %> + <%= MethodsInheritedTemplate.new(type, ancestor, ancestor.class_methods, "Class") %> + <%= MacrosInheritedTemplate.new(type, ancestor, ancestor.macros) %> + <% end %> +
    -<%= MethodDetailTemplate.new("Constructor Detail", type.constructors) %> -<%= MethodDetailTemplate.new(type.program? ? "Method Detail" : "Class Method Detail", type.class_methods) %> -<%= MethodDetailTemplate.new("Macro Detail", type.macros) %> -<%= MethodDetailTemplate.new("Instance Method Detail", type.instance_methods) %> + <%= MethodDetailTemplate.new("Constructor Detail", type.constructors) %> + <%= MethodDetailTemplate.new(type.program? ? "Method Detail" : "Class Method Detail", type.class_methods) %> + <%= MethodDetailTemplate.new("Macro Detail", type.macros) %> + <%= MethodDetailTemplate.new("Instance Method Detail", type.instance_methods) %> +<% end %>

diff --git a/src/compiler/crystal/tools/doc/macro.cr b/src/compiler/crystal/tools/doc/macro.cr index 629eccc2e225..596c6d144ec9 100644 --- a/src/compiler/crystal/tools/doc/macro.cr +++ b/src/compiler/crystal/tools/doc/macro.cr @@ -58,6 +58,9 @@ class Crystal::Doc::Macro @type.visibility end + def real_name + end + def kind "macro " end diff --git a/src/compiler/crystal/tools/doc/method.cr b/src/compiler/crystal/tools/doc/method.cr index 7f71a9c42440..8484965b72a7 100644 --- a/src/compiler/crystal/tools/doc/method.cr +++ b/src/compiler/crystal/tools/doc/method.cr @@ -124,11 +124,17 @@ class Crystal::Doc::Method @generator.relative_location(@def) end + def external_var? + (ext = @def).is_a?(External) && ext.external_var? + end + def prefix case when @type.program? "" - when @class_method + when external_var? + "$" + when @class_method, @type.lib? "." else "#" @@ -145,6 +151,12 @@ class Crystal::Doc::Method end end + def real_name + if (d = @def).is_a?(External) && (real_name = d.real_name) && (real_name != d.name) + " = #{real_name}" + end + end + def constructor? return false unless @class_method return true if name == "new" @@ -184,6 +196,10 @@ class Crystal::Doc::Method def kind case + when external_var? + "$" + when @type.lib? + "fun " when @type.program? "def " when @class_method @@ -196,7 +212,11 @@ class Crystal::Doc::Method def id String.build do |io| io << to_s.delete(' ') - if @class_method + if external_var? + io << "-external-var" + elsif @type.lib? + io << "-function" + elsif @class_method io << "-class-method" else io << "-instance-method" @@ -330,6 +350,7 @@ class Crystal::Doc::Method builder.object do builder.field "html_id", id builder.field "name", name + builder.field "real_name", real_name if real_name builder.field "doc", doc unless doc.nil? builder.field "summary", formatted_summary unless formatted_summary.nil? builder.field "abstract", abstract? @@ -338,7 +359,8 @@ class Crystal::Doc::Method builder.field "args_string", args_to_s unless args.empty? builder.field "args_html", args_to_html unless args.empty? builder.field "location", location unless location.nil? - builder.field "def", self.def + builder.field @type.lib? ? "fun" : "def", self.def + builder.field "external_var", external_var? end end diff --git a/src/compiler/crystal/tools/doc/type.cr b/src/compiler/crystal/tools/doc/type.cr index 69857cbd3306..7f3e74e173d7 100644 --- a/src/compiler/crystal/tools/doc/type.cr +++ b/src/compiler/crystal/tools/doc/type.cr @@ -20,6 +20,8 @@ class Crystal::Doc::Type case @type when Const "const" + when .extern_union? + "union" when .struct? "struct" when .class?, .metaclass? @@ -35,7 +37,9 @@ class Crystal::Doc::Type when AnnotationType "annotation" when LibType - "module" + "lib" + when TypeDefType + "type" else raise "Unhandled type in `kind`: #{@type}" end @@ -159,6 +163,18 @@ class Crystal::Doc::Type @type.is_a?(Const) end + def type_def? + @type.is_a?(TypeDefType) + end + + def lib? + @type.is_a?(LibType) + end + + def fun_def? + @type.is_a?(FunDef) + end + def alias_definition alias_def = @type.as?(AliasType).try(&.aliased_type) alias_def @@ -168,6 +184,14 @@ class Crystal::Doc::Type type_to_html alias_definition.as(Crystal::Type) end + def type_definition + @type.as?(TypeDefType).try(&.typedef) + end + + def formatted_type_definition + type_to_html type_definition.as(Crystal::Type) + end + @types : Array(Type)? def types @@ -196,6 +220,56 @@ class Crystal::Doc::Type end end + @external_vars : Array(Method)? + + def external_vars + @external_vars ||= begin + case @type + when LibType + defs = [] of Method + @type.defs.try &.each do |def_name, defs_with_metadata| + defs_with_metadata.each do |def_with_metadata| + next unless (ext = def_with_metadata.def).is_a?(External) + next if !ext.external_var? || ext.name.ends_with?("=") + next unless @generator.must_include? ext + + defs << method(ext, false) + end + end + defs.sort_by! { |x| sort_order(x) } + else + [] of Method + end + end + end + + @functions : Array(Method)? + + def functions + @functions ||= begin + case @type + when LibType + defs = [] of Method + @type.defs.try &.each do |def_name, defs_with_metadata| + defs_with_metadata.each do |def_with_metadata| + next unless (ext = def_with_metadata.def).is_a?(External) + next if ext.external_var? + next unless @generator.must_include? def_with_metadata.def + + defs << method(def_with_metadata.def, false) + end + end + defs.sort_by! { |x| sort_order(x) } + else + [] of Method + end + end + end + + private def showdoc?(adef) + @generator.showdoc?(adef.doc.try &.strip) || @generator.showdoc?(@type) + end + private def showdoc?(adef) @generator.showdoc?(adef.doc.try &.strip) || @generator.showdoc?(@type) end diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr index 3a2a759b3158..7df02924793f 100644 --- a/src/compiler/crystal/types.cr +++ b/src/compiler/crystal/types.cr @@ -2688,19 +2688,6 @@ module Crystal self end - def add_var(name, type, real_name, thread_local) - setter = External.new("#{name}=", [Arg.new("value", type: type)], Primitive.new("external_var_set", type), real_name) - setter.set_type(type) - setter.thread_local = thread_local - - getter = External.new("#{name}", [] of Arg, Primitive.new("external_var_get", type), real_name) - getter.set_type(type) - getter.thread_local = thread_local - - add_def setter - add_def getter - end - def lookup_var(name) a_def = lookup_first_def(name, false) return nil unless a_def