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 public DocumentationBuilder (ModuleContainer module)
52 doc_module = new ModuleContainer (module.Compiler);
53 doc_module.DocumentationBuilder = this;
56 XmlDocumentation = new XmlDocument ();
57 XmlDocumentation.PreserveWhitespace = false;
62 return module.Compiler.Report;
66 public MemberName ParsedName {
70 public List<DocumentationParameter> ParsedParameters {
74 public TypeExpression ParsedBuiltinType {
78 public Operator.OpType? ParsedOperator {
82 XmlNode GetDocCommentNode (MemberCore mc, string name)
84 // FIXME: It could be even optimizable as not
85 // to use XmlDocument. But anyways the nodes
86 // are not kept in memory.
87 XmlDocument doc = XmlDocumentation;
89 XmlElement el = doc.CreateElement ("member");
90 el.SetAttribute ("name", name);
91 string normalized = mc.DocComment;
92 el.InnerXml = normalized;
93 // csc keeps lines as written in the sources
94 // and inserts formatting indentation (which
95 // is different from XmlTextWriter.Formatting
96 // one), but when a start tag contains an
97 // endline, it joins the next line. We don't
98 // have to follow such a hacky behavior.
100 normalized.Split ('\n');
102 for (int i = 0; i < split.Length; i++) {
103 string s = split [i].TrimEnd ();
107 el.InnerXml = line_head + String.Join (
108 line_head, split, 0, j);
110 } catch (Exception ex) {
111 Report.Warning (1570, 1, mc.Location, "XML documentation comment on `{0}' is not well-formed XML markup ({1})",
112 mc.GetSignatureForError (), ex.Message);
114 return doc.CreateComment (String.Format ("FIXME: Invalid documentation markup was found for member {0}", name));
119 // Generates xml doc comments (if any), and if required,
120 // handle warning report.
122 internal void GenerateDocumentationForMember (MemberCore mc)
124 string name = mc.DocCommentHeader + mc.GetSignatureForDocumentation ();
126 XmlNode n = GetDocCommentNode (mc, name);
128 XmlElement el = n as XmlElement;
130 var pm = mc as IParametersMember;
132 CheckParametersComments (mc, pm, el);
135 // FIXME: it could be done with XmlReader
136 XmlNodeList nl = n.SelectNodes (".//include");
138 // It could result in current node removal, so prepare another list to iterate.
139 var al = new List<XmlNode> (nl.Count);
140 foreach (XmlNode inc in nl)
142 foreach (XmlElement inc in al)
143 if (!HandleInclude (mc, inc))
144 inc.ParentNode.RemoveChild (inc);
147 // FIXME: it could be done with XmlReader
148 var ds_target = mc as TypeContainer;
149 if (ds_target == null)
150 ds_target = mc.Parent;
152 foreach (XmlElement see in n.SelectNodes (".//see"))
153 HandleSee (mc, ds_target, see);
154 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
155 HandleSeeAlso (mc, ds_target, seealso);
156 foreach (XmlElement see in n.SelectNodes (".//exception"))
157 HandleException (mc, ds_target, see);
158 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
159 HandleTypeParam (mc, node);
160 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
161 HandleTypeParamRef (mc, node);
164 n.WriteTo (XmlCommentOutput);
168 // Processes "include" element. Check included file and
169 // embed the document content inside this documentation node.
171 bool HandleInclude (MemberCore mc, XmlElement el)
173 bool keep_include_node = false;
174 string file = el.GetAttribute ("file");
175 string path = el.GetAttribute ("path");
177 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
178 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
179 keep_include_node = true;
181 else if (path.Length == 0) {
182 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
183 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
184 keep_include_node = true;
188 if (!StoredDocuments.TryGetValue (file, out doc)) {
190 doc = new XmlDocument ();
192 StoredDocuments.Add (file, doc);
193 } catch (Exception) {
194 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
195 Report.Warning (1592, 1, mc.Location, "Badly formed XML in included comments file -- `{0}'", file);
200 XmlNodeList nl = doc.SelectNodes (path);
202 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
204 keep_include_node = true;
206 foreach (XmlNode n in nl)
207 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
208 } catch (Exception ex) {
209 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
210 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}' ({2})", path, file, ex.Message);
214 return keep_include_node;
218 // Handles <see> elements.
220 void HandleSee (MemberCore mc, TypeContainer ds, XmlElement see)
222 HandleXrefCommon (mc, ds, see);
226 // Handles <seealso> elements.
228 void HandleSeeAlso (MemberCore mc, TypeContainer ds, XmlElement seealso)
230 HandleXrefCommon (mc, ds, seealso);
234 // Handles <exception> elements.
236 void HandleException (MemberCore mc, TypeContainer ds, XmlElement seealso)
238 HandleXrefCommon (mc, ds, seealso);
242 // Handles <typeparam /> node
244 static void HandleTypeParam (MemberCore mc, XmlElement node)
246 if (!node.HasAttribute ("name"))
249 string tp_name = node.GetAttribute ("name");
250 if (mc.CurrentTypeParameters != null) {
251 if (mc.CurrentTypeParameters.Find (tp_name) != null)
255 // TODO: CS1710, CS1712
257 mc.Compiler.Report.Warning (1711, 2, mc.Location,
258 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
259 mc.GetSignatureForError (), tp_name);
263 // Handles <typeparamref /> node
265 static void HandleTypeParamRef (MemberCore mc, XmlElement node)
267 if (!node.HasAttribute ("name"))
270 string tp_name = node.GetAttribute ("name");
273 if (member.CurrentTypeParameters != null) {
274 if (member.CurrentTypeParameters.Find (tp_name) != null)
278 member = member.Parent;
279 } while (member != null);
281 mc.Compiler.Report.Warning (1735, 2, mc.Location,
282 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
283 mc.GetSignatureForError (), tp_name);
286 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
289 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
291 var left = ResolveMemberName (context, mn.Left);
292 var ns = left as Namespace;
294 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
296 TypeExpr texpr = left as TypeExpr;
298 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
300 return new TypeExpression (found, Location.Null);
309 // Processes "see" or "seealso" elements from cref attribute.
311 void HandleXrefCommon (MemberCore mc, TypeContainer ds, XmlElement xref)
313 string cref = xref.GetAttribute ("cref");
314 // when, XmlReader, "if (cref == null)"
315 if (!xref.HasAttribute ("cref"))
318 // Nothing to be resolved the reference is marked explicitly
319 if (cref.Length > 2 && cref [1] == ':')
322 // Additional symbols for < and > are allowed for easier XML typing
323 cref = cref.Replace ('{', '<').Replace ('}', '>');
325 var encoding = module.Compiler.Settings.Encoding;
326 var s = new MemoryStream (encoding.GetBytes (cref));
327 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding);
329 var source_file = new CompilationSourceFile (doc_module);
330 var report = new Report (doc_module.Compiler, new NullReportPrinter ());
332 var parser = new CSharpParser (seekable, source_file, report);
333 ParsedParameters = null;
335 ParsedBuiltinType = null;
336 ParsedOperator = null;
337 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
338 parser.Lexer.parsing_generic_declaration_doc = true;
340 if (report.Errors > 0) {
341 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
342 mc.GetSignatureForError (), cref);
344 xref.SetAttribute ("cref", "!:" + cref);
349 string prefix = null;
350 FullNamedExpression fne = null;
353 // Try built-in type first because we are using ParsedName as identifier of
354 // member names on built-in types
356 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
357 member = ParsedBuiltinType.Type;
362 if (ParsedName != null || ParsedOperator.HasValue) {
363 TypeSpec type = null;
364 string member_name = null;
366 if (member == null) {
367 if (ParsedOperator.HasValue) {
368 type = mc.CurrentType;
369 } else if (ParsedName.Left != null) {
370 fne = ResolveMemberName (mc, ParsedName.Left);
372 var ns = fne as Namespace;
374 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
383 fne = ResolveMemberName (mc, ParsedName);
385 type = mc.CurrentType;
386 } else if (ParsedParameters == null) {
388 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
389 member_name = Constructor.ConstructorName;
394 type = (TypeSpec) member;
398 if (ParsedParameters != null) {
399 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
401 var context = new DocumentationMemberContext (mc, ParsedName ?? MemberName.Null);
403 foreach (var pp in ParsedParameters) {
404 pp.Resolve (context);
407 mc.Module.Compiler.Report.SetPrinter (old_printer);
412 if (member_name == null)
413 member_name = ParsedOperator.HasValue ?
414 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
416 int parsed_param_count;
417 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
418 parsed_param_count = ParsedParameters.Count - 1;
419 } else if (ParsedParameters != null) {
420 parsed_param_count = ParsedParameters.Count;
422 parsed_param_count = 0;
425 int parameters_match = -1;
427 var members = MemberCache.FindMembers (type, member_name, true);
428 if (members != null) {
429 foreach (var m in members) {
430 if (ParsedName != null && m.Arity != ParsedName.Arity)
433 if (ParsedParameters != null) {
434 IParametersMember pm = m as IParametersMember;
438 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
441 var pm_params = pm.Parameters;
444 for (i = 0; i < parsed_param_count; ++i) {
445 var pparam = ParsedParameters[i];
447 if (i >= pm_params.Count || pparam == null || pparam.TypeSpec == null ||
448 !TypeSpecComparer.Override.IsEqual (pparam.TypeSpec, pm_params.Types[i]) ||
449 (pparam.Modifier & Parameter.Modifier.RefOutMask) != (pm_params.FixedParameters[i].ModFlags & Parameter.Modifier.RefOutMask)) {
451 if (i > parameters_match) {
452 parameters_match = i;
463 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
464 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
465 parameters_match = parsed_param_count + 1;
469 if (parsed_param_count != pm_params.Count)
474 if (member != null) {
475 Report.Warning (419, 3, mc.Location,
476 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
477 cref, member.GetSignatureForError (), m.GetSignatureForError ());
486 // Continue with parent type for nested types
487 if (member == null) {
488 type = type.DeclaringType;
492 } while (type != null);
494 if (member == null && parameters_match >= 0) {
495 for (int i = parameters_match; i < parsed_param_count; ++i) {
496 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
497 (i + 1).ToString (), cref);
500 if (parameters_match == parsed_param_count + 1) {
501 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
507 if (member == null) {
508 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
509 mc.GetSignatureForError (), cref);
511 } else if (member == InternalType.Namespace) {
512 cref = "N:" + fne.GetSignatureForError ();
514 prefix = GetMemberDocHead (member);
515 cref = prefix + member.GetSignatureForDocumentation ();
518 xref.SetAttribute ("cref", cref);
522 // Get a prefix from member type for XML documentation (used
523 // to formalize cref target name).
525 static string GetMemberDocHead (MemberSpec type)
527 if (type is FieldSpec)
529 if (type is MethodSpec)
531 if (type is EventSpec)
533 if (type is PropertySpec)
535 if (type is TypeSpec)
538 throw new NotImplementedException (type.GetType ().ToString ());
542 // Raised (and passed an XmlElement that contains the comment)
543 // when GenerateDocComment is writing documentation expectedly.
545 // FIXME: with a few effort, it could be done with XmlReader,
546 // that means removal of DOM use.
548 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
550 HashSet<string> found_tags = null;
551 foreach (XmlElement pelem in el.SelectNodes ("param")) {
552 string xname = pelem.GetAttribute ("name");
553 if (xname.Length == 0)
554 continue; // really? but MS looks doing so
556 if (found_tags == null) {
557 found_tags = new HashSet<string> ();
560 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
561 Report.Warning (1572, 2, member.Location,
562 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
563 member.GetSignatureForError (), xname);
567 if (found_tags.Contains (xname)) {
568 Report.Warning (1571, 2, member.Location,
569 "XML comment on `{0}' has a duplicate param tag for `{1}'",
570 member.GetSignatureForError (), xname);
574 found_tags.Add (xname);
577 if (found_tags != null) {
578 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
579 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
580 Report.Warning (1573, 4, member.Location,
581 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
582 p.Name, member.GetSignatureForError ());
588 // Outputs XML documentation comment from tokenized comments.
590 public bool OutputDocComment (string asmfilename, string xmlFileName)
592 XmlTextWriter w = null;
594 w = new XmlTextWriter (xmlFileName, null);
596 w.Formatting = Formatting.Indented;
597 w.WriteStartDocument ();
598 w.WriteStartElement ("doc");
599 w.WriteStartElement ("assembly");
600 w.WriteStartElement ("name");
601 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
602 w.WriteEndElement (); // name
603 w.WriteEndElement (); // assembly
604 w.WriteStartElement ("members");
605 XmlCommentOutput = w;
606 module.GenerateDocComment (this);
607 w.WriteFullEndElement (); // members
608 w.WriteEndElement ();
609 w.WriteWhitespace (Environment.NewLine);
610 w.WriteEndDocument ();
612 } catch (Exception ex) {
613 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
623 // Type lookup of documentation references uses context of type where
624 // the reference is used but type parameters from cref value
626 sealed class DocumentationMemberContext : IMemberContext
628 readonly MemberCore host;
629 MemberName contextName;
631 public DocumentationMemberContext (MemberCore host, MemberName contextName)
634 this.contextName = contextName;
637 public TypeSpec CurrentType {
639 return host.CurrentType;
643 public TypeParameters CurrentTypeParameters {
645 return contextName.TypeParameters;
649 public MemberCore CurrentMemberDefinition {
651 return host.CurrentMemberDefinition;
655 public bool IsObsolete {
661 public bool IsUnsafe {
663 return host.IsStatic;
667 public bool IsStatic {
669 return host.IsStatic;
673 public ModuleContainer Module {
679 public string GetSignatureForError ()
681 return host.GetSignatureForError ();
684 public ExtensionMethodCandidates LookupExtensionMethod (TypeSpec extensionType, string name, int arity)
689 public FullNamedExpression LookupNamespaceOrType (string name, int arity, LookupMode mode, Location loc)
692 var tp = CurrentTypeParameters;
694 for (int i = 0; i < tp.Count; ++i) {
696 if (t.Name == name) {
697 t.Type.DeclaredPosition = i;
698 return new TypeParameterExpr (t, loc);
704 return host.Parent.LookupNamespaceOrType (name, arity, mode, loc);
707 public FullNamedExpression LookupNamespaceAlias (string name)
709 throw new NotImplementedException ();
713 class DocumentationParameter
715 public readonly Parameter.Modifier Modifier;
716 public FullNamedExpression Type;
719 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
722 this.Modifier = modifier;
725 public DocumentationParameter (FullNamedExpression type)
730 public TypeSpec TypeSpec {
736 public void Resolve (IMemberContext context)
738 type = Type.ResolveAsType (context);