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 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 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 ());
400 foreach (var pp in ParsedParameters) {
403 mc.Module.Compiler.Report.SetPrinter (old_printer);
407 if (member_name == null)
408 member_name = ParsedOperator.HasValue ?
409 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
411 int parsed_param_count;
412 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
413 parsed_param_count = ParsedParameters.Count - 1;
414 } else if (ParsedParameters != null) {
415 parsed_param_count = ParsedParameters.Count;
417 parsed_param_count = 0;
420 int parameters_match = -1;
422 var members = MemberCache.FindMembers (type, member_name, true);
423 if (members != null) {
424 foreach (var m in members) {
425 if (ParsedName != null && m.Arity != ParsedName.Arity)
428 if (ParsedParameters != null) {
429 IParametersMember pm = m as IParametersMember;
433 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
437 for (i = 0; i < parsed_param_count; ++i) {
438 var pparam = ParsedParameters[i];
440 if (i >= pm.Parameters.Count || pparam == null ||
441 pparam.TypeSpec != pm.Parameters.Types[i] ||
442 (pparam.Modifier & Parameter.Modifier.SignatureMask) != (pm.Parameters.FixedParameters[i].ModFlags & Parameter.Modifier.SignatureMask)) {
444 if (i > parameters_match) {
445 parameters_match = i;
456 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
457 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
458 parameters_match = parsed_param_count + 1;
462 if (parsed_param_count != pm.Parameters.Count)
467 if (member != null) {
468 Report.Warning (419, 3, mc.Location,
469 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
470 cref, member.GetSignatureForError (), m.GetSignatureForError ());
479 // Continue with parent type for nested types
480 if (member == null) {
481 type = type.DeclaringType;
485 } while (type != null);
487 if (member == null && parameters_match >= 0) {
488 for (int i = parameters_match; i < parsed_param_count; ++i) {
489 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
490 (i + 1).ToString (), cref);
493 if (parameters_match == parsed_param_count + 1) {
494 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
500 if (member == null) {
501 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
502 mc.GetSignatureForError (), cref);
504 } else if (member == InternalType.Namespace) {
505 cref = "N:" + fne.GetSignatureForError ();
507 prefix = GetMemberDocHead (member);
508 cref = prefix + member.GetSignatureForDocumentation ();
511 xref.SetAttribute ("cref", cref);
515 // Get a prefix from member type for XML documentation (used
516 // to formalize cref target name).
518 static string GetMemberDocHead (MemberSpec type)
520 if (type is FieldSpec)
522 if (type is MethodSpec)
524 if (type is EventSpec)
526 if (type is PropertySpec)
528 if (type is TypeSpec)
531 throw new NotImplementedException (type.GetType ().ToString ());
535 // Raised (and passed an XmlElement that contains the comment)
536 // when GenerateDocComment is writing documentation expectedly.
538 // FIXME: with a few effort, it could be done with XmlReader,
539 // that means removal of DOM use.
541 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
543 HashSet<string> found_tags = null;
544 foreach (XmlElement pelem in el.SelectNodes ("param")) {
545 string xname = pelem.GetAttribute ("name");
546 if (xname.Length == 0)
547 continue; // really? but MS looks doing so
549 if (found_tags == null) {
550 found_tags = new HashSet<string> ();
553 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
554 Report.Warning (1572, 2, member.Location,
555 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
556 member.GetSignatureForError (), xname);
560 if (found_tags.Contains (xname)) {
561 Report.Warning (1571, 2, member.Location,
562 "XML comment on `{0}' has a duplicate param tag for `{1}'",
563 member.GetSignatureForError (), xname);
567 found_tags.Add (xname);
570 if (found_tags != null) {
571 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
572 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
573 Report.Warning (1573, 4, member.Location,
574 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
575 p.Name, member.GetSignatureForError ());
581 // Outputs XML documentation comment from tokenized comments.
583 public bool OutputDocComment (string asmfilename, string xmlFileName)
585 XmlTextWriter w = null;
587 w = new XmlTextWriter (xmlFileName, null);
589 w.Formatting = Formatting.Indented;
590 w.WriteStartDocument ();
591 w.WriteStartElement ("doc");
592 w.WriteStartElement ("assembly");
593 w.WriteStartElement ("name");
594 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
595 w.WriteEndElement (); // name
596 w.WriteEndElement (); // assembly
597 w.WriteStartElement ("members");
598 XmlCommentOutput = w;
599 module.GenerateDocComment (this);
600 w.WriteFullEndElement (); // members
601 w.WriteEndElement ();
602 w.WriteWhitespace (Environment.NewLine);
603 w.WriteEndDocument ();
605 } catch (Exception ex) {
606 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
615 class DocumentationParameter
617 public readonly Parameter.Modifier Modifier;
618 public FullNamedExpression Type;
621 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
624 this.Modifier = modifier;
627 public DocumentationParameter (FullNamedExpression type)
632 public TypeSpec TypeSpec {
638 public void Resolve (IMemberContext context)
640 type = Type.ResolveAsType (context);