Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :showdoc: directive (RFC #11) #15294

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions spec/compiler/parser/parser_doc_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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(%(
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/compiler/crystal/semantic/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
18 changes: 13 additions & 5 deletions src/compiler/crystal/semantic/top_level_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 33 additions & 3 deletions src/compiler/crystal/semantic/type_declaration_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ module Crystal
include SpecialVar

property name : String
property doc : String?

def initialize(@name : String)
end
Expand Down Expand Up @@ -1630,6 +1631,7 @@ module Crystal
end

class TypeDeclaration < ASTNode
property doc : String?
property var : ASTNode
property declared_type : ASTNode
property value : ASTNode?
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1980,6 +1983,7 @@ module Crystal

class TypeDef < ASTNode
property name : String
property doc : String?
property type_spec : ASTNode
property name_location : Location?

Expand All @@ -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

Expand Down Expand Up @@ -2044,6 +2049,7 @@ module Crystal

class ExternalVar < ASTNode
property name : String
property doc : String?
property type_spec : ASTNode
property real_name : String?

Expand Down
14 changes: 13 additions & 1 deletion src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5633,6 +5633,7 @@ module Crystal
end

def parse_lib
doc = @token.doc
location = @token.location
next_token_skip_space_or_newline

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
33 changes: 27 additions & 6 deletions src/compiler/crystal/tools/doc/generator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class Crystal::Doc::Generator
end

def must_include?(type : Crystal::Type)
return false if type.private?
return false if type.private? && !showdoc?(type)
return false if nodoc? type
return true if crystal_builtin?(type)

Expand All @@ -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
Expand Down Expand Up @@ -215,6 +217,25 @@ class Crystal::Doc::Generator
nodoc? obj.doc.try &.strip
end

def showdoc?(str : String?) : Bool
return false if !str || [email protected]_doc?
str.starts_with?(":showdoc:")
end

def showdoc?(obj : Crystal::Type)
return false if [email protected]_doc?

if showdoc?(obj.doc.try &.strip)
return true
end

obj.each_namespace do |ns|
return true if showdoc?(ns.doc.try &.strip)
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
Expand Down Expand Up @@ -253,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
Expand All @@ -267,7 +288,7 @@ class Crystal::Doc::Generator
types = [] of Constant

parent.type.types?.try &.each_value do |type|
if type.is_a?(Const) && must_include?(type) && !type.private?
if type.is_a?(Const) && must_include?(type) && (!type.private? || showdoc?(type))
types << Constant.new(self, parent, type)
end
end
Expand Down Expand Up @@ -296,7 +317,7 @@ class Crystal::Doc::Generator
end

def doc(obj : Type | Method | Macro | Constant)
doc = obj.doc
doc = obj.doc.try &.strip.lchop(":showdoc:")

return if !doc && !has_doc_annotations?(obj)

Expand Down
4 changes: 2 additions & 2 deletions src/compiler/crystal/tools/doc/html/_method_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ <h2>
<% methods.each do |method| %>
<div class="entry-detail" id="<%= method.html_id %>">
<div class="signature">
<%= method.abstract? ? "abstract " : "" %>
<%= method.kind %><strong><%= method.name %></strong><%= method.args_to_html %>
<%= method.abstract? ? "abstract " : "" %><%= method.visibility %>
<%= method.kind %><strong><%= method.name %></strong><%= method.real_name %><%= method.args_to_html %>

<a class="method-permalink" href="<%= method.anchor %>">#</a>
</div>
Expand Down
Loading
Loading