2 // doc.cs: Support for XML documentation comment.
5 // Atsushi Enomoto <atsushi@ximian.com>
6 // Marek Safar (marek.safar@gmail.com>
8 // Dual licensed under the terms of the MIT X11 or GNU GPL
10 // Copyright 2004 Novell, Inc.
11 // Copyright 2011 Xamarin Inc
16 using System.Collections.Generic;
25 // Implements XML documentation generation.
27 class DocumentationBuilder
30 // Used to create element which helps well-formedness checking.
32 readonly XmlDocument XmlDocumentation;
34 readonly ModuleContainer module;
35 readonly ModuleContainer doc_module;
38 // The output for XML documentation.
40 XmlWriter XmlCommentOutput;
42 static readonly string line_head = Environment.NewLine + " ";
45 // Stores XmlDocuments that are included in XML documentation.
46 // Keys are included filenames, values are XmlDocuments.
48 Dictionary<string, XmlDocument> StoredDocuments = new Dictionary<string, XmlDocument> ();
50 ParserSession session;
52 public DocumentationBuilder (ModuleContainer module)
54 doc_module = new ModuleContainer (module.Compiler);
55 doc_module.DocumentationBuilder = this;
58 XmlDocumentation = new XmlDocument ();
59 XmlDocumentation.PreserveWhitespace = false;
64 return module.Compiler.Report;
68 public MemberName ParsedName {
72 public List<DocumentationParameter> ParsedParameters {
76 public TypeExpression ParsedBuiltinType {
80 public Operator.OpType? ParsedOperator {
84 XmlNode GetDocCommentNode (MemberCore mc, string name)
86 // FIXME: It could be even optimizable as not
87 // to use XmlDocument. But anyways the nodes
88 // are not kept in memory.
89 XmlDocument doc = XmlDocumentation;
91 XmlElement el = doc.CreateElement ("member");
92 el.SetAttribute ("name", name);
93 string normalized = mc.DocComment;
94 el.InnerXml = normalized;
96 string [] split = normalized.Split ('\n');
97 el.InnerXml = line_head + String.Join (line_head, split);
99 } catch (Exception ex) {
100 Report.Warning (1570, 1, mc.Location, "XML documentation comment on `{0}' is not well-formed XML markup ({1})",
101 mc.GetSignatureForError (), ex.Message);
103 return doc.CreateComment (String.Format ("FIXME: Invalid documentation markup was found for member {0}", name));
108 // Generates xml doc comments (if any), and if required,
109 // handle warning report.
111 internal void GenerateDocumentationForMember (MemberCore mc)
113 string name = mc.DocCommentHeader + mc.GetSignatureForDocumentation ();
115 XmlNode n = GetDocCommentNode (mc, name);
117 XmlElement el = n as XmlElement;
119 var pm = mc as IParametersMember;
121 CheckParametersComments (mc, pm, el);
124 // FIXME: it could be done with XmlReader
125 XmlNodeList nl = n.SelectNodes (".//include");
127 // It could result in current node removal, so prepare another list to iterate.
128 var al = new List<XmlNode> (nl.Count);
129 foreach (XmlNode inc in nl)
131 foreach (XmlElement inc in al)
132 if (!HandleInclude (mc, inc))
133 inc.ParentNode.RemoveChild (inc);
136 // FIXME: it could be done with XmlReader
138 foreach (XmlElement see in n.SelectNodes (".//see"))
140 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
141 HandleSeeAlso (mc, seealso);
142 foreach (XmlElement see in n.SelectNodes (".//exception"))
143 HandleException (mc, see);
144 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
145 HandleTypeParam (mc, node);
146 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
147 HandleTypeParamRef (mc, node);
150 n.WriteTo (XmlCommentOutput);
154 // Processes "include" element. Check included file and
155 // embed the document content inside this documentation node.
157 bool HandleInclude (MemberCore mc, XmlElement el)
159 bool keep_include_node = false;
160 string file = el.GetAttribute ("file");
161 string path = el.GetAttribute ("path");
164 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
165 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
166 keep_include_node = true;
167 } else if (path.Length == 0) {
168 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
169 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
170 keep_include_node = true;
173 Exception exception = null;
174 var full_path = Path.Combine (Path.GetDirectoryName (mc.Location.NameFullPath), file);
176 if (!StoredDocuments.TryGetValue (full_path, out doc)) {
178 doc = new XmlDocument ();
179 doc.Load (full_path);
180 StoredDocuments.Add (full_path, doc);
181 } catch (Exception e) {
183 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
189 XmlNodeList nl = doc.SelectNodes (path);
191 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
193 keep_include_node = true;
195 foreach (XmlNode n in nl)
196 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
197 } catch (Exception ex) {
199 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
203 if (exception != null) {
204 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}'. {2}",
205 path, file, exception.Message);
209 return keep_include_node;
213 // Handles <see> elements.
215 void HandleSee (MemberCore mc, XmlElement see)
217 HandleXrefCommon (mc, see);
221 // Handles <seealso> elements.
223 void HandleSeeAlso (MemberCore mc, XmlElement seealso)
225 HandleXrefCommon (mc, seealso);
229 // Handles <exception> elements.
231 void HandleException (MemberCore mc, XmlElement seealso)
233 HandleXrefCommon (mc, seealso);
237 // Handles <typeparam /> node
239 static void HandleTypeParam (MemberCore mc, XmlElement node)
241 if (!node.HasAttribute ("name"))
244 string tp_name = node.GetAttribute ("name");
245 if (mc.CurrentTypeParameters != null) {
246 if (mc.CurrentTypeParameters.Find (tp_name) != null)
250 // TODO: CS1710, CS1712
252 mc.Compiler.Report.Warning (1711, 2, mc.Location,
253 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
254 mc.GetSignatureForError (), tp_name);
258 // Handles <typeparamref /> node
260 static void HandleTypeParamRef (MemberCore mc, XmlElement node)
262 if (!node.HasAttribute ("name"))
265 string tp_name = node.GetAttribute ("name");
268 if (member.CurrentTypeParameters != null) {
269 if (member.CurrentTypeParameters.Find (tp_name) != null)
273 member = member.Parent;
274 } while (member != null);
276 mc.Compiler.Report.Warning (1735, 2, mc.Location,
277 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
278 mc.GetSignatureForError (), tp_name);
281 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
284 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
286 var left = ResolveMemberName (context, mn.Left);
287 var ns = left as NamespaceExpression;
289 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
291 TypeExpr texpr = left as TypeExpr;
293 var found = MemberCache.FindNestedType (texpr.Type, mn.Name, mn.Arity, false);
295 return new TypeExpression (found, Location.Null);
304 // Processes "see" or "seealso" elements from cref attribute.
306 void HandleXrefCommon (MemberCore mc, XmlElement xref)
308 string cref = xref.GetAttribute ("cref");
309 // when, XmlReader, "if (cref == null)"
310 if (!xref.HasAttribute ("cref"))
313 // Nothing to be resolved the reference is marked explicitly
314 if (cref.Length > 2 && cref [1] == ':')
317 // Additional symbols for < and > are allowed for easier XML typing
318 cref = cref.Replace ('{', '<').Replace ('}', '>');
320 var encoding = module.Compiler.Settings.Encoding;
321 var s = new MemoryStream (encoding.GetBytes (cref));
323 var source_file = new CompilationSourceFile (doc_module, mc.Location.SourceFile);
324 var report = new Report (doc_module.Compiler, new NullReportPrinter ());
327 session = new ParserSession {
328 UseJayGlobalArrays = true
331 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding, session.StreamReaderBuffer);
333 var parser = new CSharpParser (seekable, source_file, report, session);
334 ParsedParameters = null;
336 ParsedBuiltinType = null;
337 ParsedOperator = null;
338 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
339 parser.Lexer.parsing_generic_declaration_doc = true;
341 if (report.Errors > 0) {
342 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
343 mc.GetSignatureForError (), cref);
345 xref.SetAttribute ("cref", "!:" + cref);
350 string prefix = null;
351 FullNamedExpression fne = null;
354 // Try built-in type first because we are using ParsedName as identifier of
355 // member names on built-in types
357 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
358 member = ParsedBuiltinType.Type;
363 if (ParsedName != null || ParsedOperator.HasValue) {
364 TypeSpec type = null;
365 string member_name = null;
367 if (member == null) {
368 if (ParsedOperator.HasValue) {
369 type = mc.CurrentType;
370 } else if (ParsedName.Left != null) {
371 fne = ResolveMemberName (mc, ParsedName.Left);
373 var ns = fne as NamespaceExpression;
375 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
384 fne = ResolveMemberName (mc, ParsedName);
386 type = mc.CurrentType;
387 } else if (ParsedParameters == null) {
389 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
390 member_name = Constructor.ConstructorName;
395 type = (TypeSpec) member;
399 if (ParsedParameters != null) {
400 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
402 var context = new DocumentationMemberContext (mc, ParsedName ?? MemberName.Null);
404 foreach (var pp in ParsedParameters) {
405 pp.Resolve (context);
408 mc.Module.Compiler.Report.SetPrinter (old_printer);
413 if (member_name == null)
414 member_name = ParsedOperator.HasValue ?
415 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
417 int parsed_param_count;
418 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
419 parsed_param_count = ParsedParameters.Count - 1;
420 } else if (ParsedParameters != null) {
421 parsed_param_count = ParsedParameters.Count;
423 parsed_param_count = 0;
426 int parameters_match = -1;
428 var members = MemberCache.FindMembers (type, member_name, true);
429 if (members != null) {
430 foreach (var m in members) {
431 if (ParsedName != null && m.Arity != ParsedName.Arity)
434 if (ParsedParameters != null) {
435 IParametersMember pm = m as IParametersMember;
439 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
442 var pm_params = pm.Parameters;
445 for (i = 0; i < parsed_param_count; ++i) {
446 var pparam = ParsedParameters[i];
448 if (i >= pm_params.Count || pparam == null || pparam.TypeSpec == null ||
449 !TypeSpecComparer.Override.IsEqual (pparam.TypeSpec, pm_params.Types[i]) ||
450 (pparam.Modifier & Parameter.Modifier.RefOutMask) != (pm_params.FixedParameters[i].ModFlags & Parameter.Modifier.RefOutMask)) {
452 if (i > parameters_match) {
453 parameters_match = i;
464 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
465 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
466 parameters_match = parsed_param_count + 1;
470 if (parsed_param_count != pm_params.Count)
475 if (member != null) {
476 Report.Warning (419, 3, mc.Location,
477 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
478 cref, member.GetSignatureForError (), m.GetSignatureForError ());
487 // Continue with parent type for nested types
488 if (member == null) {
489 type = type.DeclaringType;
493 } while (type != null);
495 if (member == null && parameters_match >= 0) {
496 for (int i = parameters_match; i < parsed_param_count; ++i) {
497 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
498 (i + 1).ToString (), cref);
501 if (parameters_match == parsed_param_count + 1) {
502 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
508 if (member == null) {
509 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
510 mc.GetSignatureForError (), cref);
512 } else if (member == InternalType.Namespace) {
513 cref = "N:" + fne.GetSignatureForError ();
515 prefix = GetMemberDocHead (member);
516 cref = prefix + member.GetSignatureForDocumentation ();
519 xref.SetAttribute ("cref", cref);
523 // Get a prefix from member type for XML documentation (used
524 // to formalize cref target name).
526 static string GetMemberDocHead (MemberSpec type)
528 if (type is FieldSpec)
530 if (type is MethodSpec)
532 if (type is EventSpec)
534 if (type is PropertySpec)
536 if (type is TypeSpec)
539 throw new NotImplementedException (type.GetType ().ToString ());
543 // Raised (and passed an XmlElement that contains the comment)
544 // when GenerateDocComment is writing documentation expectedly.
546 // FIXME: with a few effort, it could be done with XmlReader,
547 // that means removal of DOM use.
549 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
551 HashSet<string> found_tags = null;
552 foreach (XmlElement pelem in el.SelectNodes ("param")) {
553 string xname = pelem.GetAttribute ("name");
554 if (xname.Length == 0)
555 continue; // really? but MS looks doing so
557 if (found_tags == null) {
558 found_tags = new HashSet<string> ();
561 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
562 Report.Warning (1572, 2, member.Location,
563 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
564 member.GetSignatureForError (), xname);
568 if (found_tags.Contains (xname)) {
569 Report.Warning (1571, 2, member.Location,
570 "XML comment on `{0}' has a duplicate param tag for `{1}'",
571 member.GetSignatureForError (), xname);
575 found_tags.Add (xname);
578 if (found_tags != null) {
579 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
580 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
581 Report.Warning (1573, 4, member.Location,
582 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
583 p.Name, member.GetSignatureForError ());
589 // Outputs XML documentation comment from tokenized comments.
591 public bool OutputDocComment (string asmfilename, string xmlFileName)
593 XmlTextWriter w = null;
595 w = new XmlTextWriter (xmlFileName, null);
597 w.Formatting = Formatting.Indented;
598 w.WriteStartDocument ();
599 w.WriteStartElement ("doc");
600 w.WriteStartElement ("assembly");
601 w.WriteStartElement ("name");
602 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
603 w.WriteEndElement (); // name
604 w.WriteEndElement (); // assembly
605 w.WriteStartElement ("members");
606 XmlCommentOutput = w;
607 module.GenerateDocComment (this);
608 w.WriteFullEndElement (); // members
609 w.WriteEndElement ();
610 w.WriteWhitespace (Environment.NewLine);
611 w.WriteEndDocument ();
613 } catch (Exception ex) {
614 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
624 // Type lookup of documentation references uses context of type where
625 // the reference is used but type parameters from cref value
627 sealed class DocumentationMemberContext : IMemberContext
629 readonly MemberCore host;
630 MemberName contextName;
632 public DocumentationMemberContext (MemberCore host, MemberName contextName)
635 this.contextName = contextName;
638 public TypeSpec CurrentType {
640 return host.CurrentType;
644 public TypeParameters CurrentTypeParameters {
646 return contextName.TypeParameters;
650 public MemberCore CurrentMemberDefinition {
652 return host.CurrentMemberDefinition;
656 public bool IsObsolete {
662 public bool IsUnsafe {
664 return host.IsStatic;
668 public bool IsStatic {
670 return host.IsStatic;
674 public ModuleContainer Module {
680 public string GetSignatureForError ()
682 return host.GetSignatureForError ();
685 public ExtensionMethodCandidates LookupExtensionMethod (string name, int arity)
690 public FullNamedExpression LookupNamespaceOrType (string name, int arity, LookupMode mode, Location loc)
693 var tp = CurrentTypeParameters;
695 for (int i = 0; i < tp.Count; ++i) {
697 if (t.Name == name) {
698 t.Type.DeclaredPosition = i;
699 return new TypeParameterExpr (t, loc);
705 return host.Parent.LookupNamespaceOrType (name, arity, mode, loc);
708 public FullNamedExpression LookupNamespaceAlias (string name)
710 throw new NotImplementedException ();
714 class DocumentationParameter
716 public readonly Parameter.Modifier Modifier;
717 public FullNamedExpression Type;
720 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
723 this.Modifier = modifier;
726 public DocumentationParameter (FullNamedExpression type)
731 public TypeSpec TypeSpec {
737 public void Resolve (IMemberContext context)
739 type = Type.ResolveAsType (context);