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;
37 // The output for XML documentation.
39 XmlWriter XmlCommentOutput;
41 static readonly string line_head = Environment.NewLine + " ";
44 // Stores XmlDocuments that are included in XML documentation.
45 // Keys are included filenames, values are XmlDocuments.
47 Dictionary<string, XmlDocument> StoredDocuments = new Dictionary<string, XmlDocument> ();
49 public DocumentationBuilder (ModuleContainer module)
52 XmlDocumentation = new XmlDocument ();
53 XmlDocumentation.PreserveWhitespace = false;
58 return module.Compiler.Report;
62 public MemberName ParsedName {
66 public List<DocumentationParameter> ParsedParameters {
70 public TypeExpression ParsedBuiltinType {
74 public Operator.OpType? ParsedOperator {
78 XmlNode GetDocCommentNode (MemberCore mc, string name)
80 // FIXME: It could be even optimizable as not
81 // to use XmlDocument. But anyways the nodes
82 // are not kept in memory.
83 XmlDocument doc = XmlDocumentation;
85 XmlElement el = doc.CreateElement ("member");
86 el.SetAttribute ("name", name);
87 string normalized = mc.DocComment;
88 el.InnerXml = normalized;
89 // csc keeps lines as written in the sources
90 // and inserts formatting indentation (which
91 // is different from XmlTextWriter.Formatting
92 // one), but when a start tag contains an
93 // endline, it joins the next line. We don't
94 // have to follow such a hacky behavior.
96 normalized.Split ('\n');
98 for (int i = 0; i < split.Length; i++) {
99 string s = split [i].TrimEnd ();
103 el.InnerXml = line_head + String.Join (
104 line_head, split, 0, j);
106 } catch (Exception ex) {
107 Report.Warning (1570, 1, mc.Location, "XML documentation comment on `{0}' is not well-formed XML markup ({1})",
108 mc.GetSignatureForError (), ex.Message);
110 return doc.CreateComment (String.Format ("FIXME: Invalid documentation markup was found for member {0}", name));
115 // Generates xml doc comments (if any), and if required,
116 // handle warning report.
118 internal void GenerateDocumentationForMember (MemberCore mc)
120 string name = mc.DocCommentHeader + mc.GetSignatureForDocumentation ();
122 XmlNode n = GetDocCommentNode (mc, name);
124 XmlElement el = n as XmlElement;
126 var pm = mc as IParametersMember;
128 CheckParametersComments (mc, pm, el);
131 // FIXME: it could be done with XmlReader
132 XmlNodeList nl = n.SelectNodes (".//include");
134 // It could result in current node removal, so prepare another list to iterate.
135 var al = new List<XmlNode> (nl.Count);
136 foreach (XmlNode inc in nl)
138 foreach (XmlElement inc in al)
139 if (!HandleInclude (mc, inc))
140 inc.ParentNode.RemoveChild (inc);
143 // FIXME: it could be done with XmlReader
144 var ds_target = mc as TypeContainer;
145 if (ds_target == null)
146 ds_target = mc.Parent;
148 foreach (XmlElement see in n.SelectNodes (".//see"))
149 HandleSee (mc, ds_target, see);
150 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
151 HandleSeeAlso (mc, ds_target, seealso);
152 foreach (XmlElement see in n.SelectNodes (".//exception"))
153 HandleException (mc, ds_target, see);
154 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
155 HandleTypeParam (mc, node);
156 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
157 HandleTypeParamRef (mc, node);
160 n.WriteTo (XmlCommentOutput);
164 // Processes "include" element. Check included file and
165 // embed the document content inside this documentation node.
167 bool HandleInclude (MemberCore mc, XmlElement el)
169 bool keep_include_node = false;
170 string file = el.GetAttribute ("file");
171 string path = el.GetAttribute ("path");
173 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
174 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
175 keep_include_node = true;
177 else if (path.Length == 0) {
178 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
179 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
180 keep_include_node = true;
184 if (!StoredDocuments.TryGetValue (file, out doc)) {
186 doc = new XmlDocument ();
188 StoredDocuments.Add (file, doc);
189 } catch (Exception) {
190 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
191 Report.Warning (1592, 1, mc.Location, "Badly formed XML in included comments file -- `{0}'", file);
196 XmlNodeList nl = doc.SelectNodes (path);
198 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
200 keep_include_node = true;
202 foreach (XmlNode n in nl)
203 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
204 } catch (Exception ex) {
205 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
206 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}' ({2})", path, file, ex.Message);
210 return keep_include_node;
214 // Handles <see> elements.
216 void HandleSee (MemberCore mc, TypeContainer ds, XmlElement see)
218 HandleXrefCommon (mc, ds, see);
222 // Handles <seealso> elements.
224 void HandleSeeAlso (MemberCore mc, TypeContainer ds, XmlElement seealso)
226 HandleXrefCommon (mc, ds, seealso);
230 // Handles <exception> elements.
232 void HandleException (MemberCore mc, TypeContainer ds, XmlElement seealso)
234 HandleXrefCommon (mc, ds, seealso);
238 // Handles <typeparam /> node
240 void HandleTypeParam (MemberCore mc, XmlElement node)
242 if (!node.HasAttribute ("name"))
245 string tp_name = node.GetAttribute ("name");
246 if (mc.CurrentTypeParameters != null) {
247 if (mc.CurrentTypeParameters.Find (tp_name) != null)
251 // TODO: CS1710, CS1712
253 mc.Compiler.Report.Warning (1711, 2, mc.Location,
254 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
255 mc.GetSignatureForError (), tp_name);
259 // Handles <typeparamref /> node
261 void HandleTypeParamRef (MemberCore mc, XmlElement node)
263 if (!node.HasAttribute ("name"))
266 string tp_name = node.GetAttribute ("name");
269 if (member.CurrentTypeParameters != null) {
270 if (member.CurrentTypeParameters.Find (tp_name) != null)
274 member = member.Parent;
275 } while (member != null);
277 mc.Compiler.Report.Warning (1735, 2, mc.Location,
278 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
279 mc.GetSignatureForError (), tp_name);
282 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
285 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
287 var left = ResolveMemberName (context, mn.Left);
288 var ns = left as Namespace;
290 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
292 TypeExpr texpr = left as TypeExpr;
294 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
296 return new TypeExpression (found, Location.Null);
305 // Processes "see" or "seealso" elements from cref attribute.
307 void HandleXrefCommon (MemberCore mc, TypeContainer ds, XmlElement xref)
309 string cref = xref.GetAttribute ("cref");
310 // when, XmlReader, "if (cref == null)"
311 if (!xref.HasAttribute ("cref"))
314 // Nothing to be resolved the reference is marked explicitly
315 if (cref.Length > 2 && cref [1] == ':')
318 // Additional symbols for < and > are allowed for easier XML typing
319 cref = cref.Replace ('{', '<').Replace ('}', '>');
321 var encoding = module.Compiler.Settings.Encoding;
322 var s = new MemoryStream (encoding.GetBytes (cref));
323 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding);
325 var source_file = new CompilationSourceFile ("{documentation}", "", 1);
326 var doc_module = new ModuleContainer (module.Compiler);
327 doc_module.DocumentationBuilder = this;
328 source_file.NamespaceContainer = new NamespaceContainer (null, doc_module, null, source_file);
330 Report parse_report = new Report (new NullReportPrinter ());
331 var parser = new CSharpParser (seekable, source_file, parse_report);
332 ParsedParameters = null;
334 ParsedBuiltinType = null;
335 ParsedOperator = null;
336 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
337 parser.Lexer.parsing_generic_declaration_doc = true;
339 if (parse_report.Errors > 0) {
340 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
341 mc.GetSignatureForError (), cref);
343 xref.SetAttribute ("cref", "!:" + cref);
348 string prefix = null;
349 FullNamedExpression fne = null;
352 // Try built-in type first because we are using ParsedName as identifier of
353 // member names on built-in types
355 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
356 member = ParsedBuiltinType.Type;
361 if (ParsedName != null || ParsedOperator.HasValue) {
362 TypeSpec type = null;
363 string member_name = null;
365 if (member == null) {
366 if (ParsedOperator.HasValue) {
367 type = mc.CurrentType;
368 } else if (ParsedName.Left != null) {
369 fne = ResolveMemberName (mc, ParsedName.Left);
371 var ns = fne as Namespace;
373 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
382 fne = ResolveMemberName (mc, ParsedName);
384 type = mc.CurrentType;
385 } else if (ParsedParameters == null) {
387 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
388 member_name = Constructor.ConstructorName;
393 type = (TypeSpec) member;
397 if (ParsedParameters != null) {
398 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
399 foreach (var pp in ParsedParameters) {
402 mc.Module.Compiler.Report.SetPrinter (old_printer);
406 if (member_name == null)
407 member_name = ParsedOperator.HasValue ?
408 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
410 int parsed_param_count;
411 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
412 parsed_param_count = ParsedParameters.Count - 1;
413 } else if (ParsedParameters != null) {
414 parsed_param_count = ParsedParameters.Count;
416 parsed_param_count = 0;
419 int parameters_match = -1;
421 var members = MemberCache.FindMembers (type, member_name, true);
422 if (members != null) {
423 foreach (var m in members) {
424 if (ParsedName != null && m.Arity != ParsedName.Arity)
427 if (ParsedParameters != null) {
428 IParametersMember pm = m as IParametersMember;
432 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
436 for (i = 0; i < parsed_param_count; ++i) {
437 var pparam = ParsedParameters[i];
439 if (i >= pm.Parameters.Count || pparam == null ||
440 pparam.TypeSpec != pm.Parameters.Types[i] ||
441 (pparam.Modifier & Parameter.Modifier.SignatureMask) != (pm.Parameters.FixedParameters[i].ModFlags & Parameter.Modifier.SignatureMask)) {
443 if (i > parameters_match) {
444 parameters_match = i;
455 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
456 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
457 parameters_match = parsed_param_count + 1;
461 if (parsed_param_count != pm.Parameters.Count)
466 if (member != null) {
467 Report.Warning (419, 3, mc.Location,
468 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
469 cref, member.GetSignatureForError (), m.GetSignatureForError ());
478 // Continue with parent type for nested types
479 if (member == null) {
480 type = type.DeclaringType;
484 } while (type != null);
486 if (member == null && parameters_match >= 0) {
487 for (int i = parameters_match; i < parsed_param_count; ++i) {
488 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
489 (i + 1).ToString (), cref);
492 if (parameters_match == parsed_param_count + 1) {
493 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
499 if (member == null) {
500 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
501 mc.GetSignatureForError (), cref);
503 } else if (member == InternalType.Namespace) {
504 cref = "N:" + fne.GetSignatureForError ();
506 prefix = GetMemberDocHead (member);
507 cref = prefix + member.GetSignatureForDocumentation ();
510 xref.SetAttribute ("cref", cref);
514 // Get a prefix from member type for XML documentation (used
515 // to formalize cref target name).
517 static string GetMemberDocHead (MemberSpec type)
519 if (type is FieldSpec)
521 if (type is MethodSpec)
523 if (type is EventSpec)
525 if (type is PropertySpec)
527 if (type is TypeSpec)
530 throw new NotImplementedException (type.GetType ().ToString ());
534 // Raised (and passed an XmlElement that contains the comment)
535 // when GenerateDocComment is writing documentation expectedly.
537 // FIXME: with a few effort, it could be done with XmlReader,
538 // that means removal of DOM use.
540 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
542 HashSet<string> found_tags = null;
543 foreach (XmlElement pelem in el.SelectNodes ("param")) {
544 string xname = pelem.GetAttribute ("name");
545 if (xname.Length == 0)
546 continue; // really? but MS looks doing so
548 if (found_tags == null) {
549 found_tags = new HashSet<string> ();
552 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
553 Report.Warning (1572, 2, member.Location,
554 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
555 member.GetSignatureForError (), xname);
559 if (found_tags.Contains (xname)) {
560 Report.Warning (1571, 2, member.Location,
561 "XML comment on `{0}' has a duplicate param tag for `{1}'",
562 member.GetSignatureForError (), xname);
566 found_tags.Add (xname);
569 if (found_tags != null) {
570 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
571 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
572 Report.Warning (1573, 4, member.Location,
573 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
574 p.Name, member.GetSignatureForError ());
580 // Outputs XML documentation comment from tokenized comments.
582 public bool OutputDocComment (string asmfilename, string xmlFileName)
584 XmlTextWriter w = null;
586 w = new XmlTextWriter (xmlFileName, null);
588 w.Formatting = Formatting.Indented;
589 w.WriteStartDocument ();
590 w.WriteStartElement ("doc");
591 w.WriteStartElement ("assembly");
592 w.WriteStartElement ("name");
593 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
594 w.WriteEndElement (); // name
595 w.WriteEndElement (); // assembly
596 w.WriteStartElement ("members");
597 XmlCommentOutput = w;
598 module.GenerateDocComment (this);
599 w.WriteFullEndElement (); // members
600 w.WriteEndElement ();
601 w.WriteWhitespace (Environment.NewLine);
602 w.WriteEndDocument ();
604 } catch (Exception ex) {
605 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
614 class DocumentationParameter
616 public readonly Parameter.Modifier Modifier;
617 public FullNamedExpression Type;
620 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
623 this.Modifier = modifier;
626 public DocumentationParameter (FullNamedExpression type)
631 public TypeSpec TypeSpec {
637 public void Resolve (IMemberContext context)
639 type = Type.ResolveAsType (context);