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;
95 // csc keeps lines as written in the sources
96 // and inserts formatting indentation (which
97 // is different from XmlTextWriter.Formatting
98 // one), but when a start tag contains an
99 // endline, it joins the next line. We don't
100 // have to follow such a hacky behavior.
102 normalized.Split ('\n');
104 for (int i = 0; i < split.Length; i++) {
105 string s = split [i].TrimEnd ();
109 el.InnerXml = line_head + String.Join (
110 line_head, split, 0, j);
112 } catch (Exception ex) {
113 Report.Warning (1570, 1, mc.Location, "XML documentation comment on `{0}' is not well-formed XML markup ({1})",
114 mc.GetSignatureForError (), ex.Message);
116 return doc.CreateComment (String.Format ("FIXME: Invalid documentation markup was found for member {0}", name));
121 // Generates xml doc comments (if any), and if required,
122 // handle warning report.
124 internal void GenerateDocumentationForMember (MemberCore mc)
126 string name = mc.DocCommentHeader + mc.GetSignatureForDocumentation ();
128 XmlNode n = GetDocCommentNode (mc, name);
130 XmlElement el = n as XmlElement;
132 var pm = mc as IParametersMember;
134 CheckParametersComments (mc, pm, el);
137 // FIXME: it could be done with XmlReader
138 XmlNodeList nl = n.SelectNodes (".//include");
140 // It could result in current node removal, so prepare another list to iterate.
141 var al = new List<XmlNode> (nl.Count);
142 foreach (XmlNode inc in nl)
144 foreach (XmlElement inc in al)
145 if (!HandleInclude (mc, inc))
146 inc.ParentNode.RemoveChild (inc);
149 // FIXME: it could be done with XmlReader
150 var ds_target = mc as TypeContainer;
151 if (ds_target == null)
152 ds_target = mc.Parent;
154 foreach (XmlElement see in n.SelectNodes (".//see"))
155 HandleSee (mc, ds_target, see);
156 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
157 HandleSeeAlso (mc, ds_target, seealso);
158 foreach (XmlElement see in n.SelectNodes (".//exception"))
159 HandleException (mc, ds_target, see);
160 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
161 HandleTypeParam (mc, node);
162 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
163 HandleTypeParamRef (mc, node);
166 n.WriteTo (XmlCommentOutput);
170 // Processes "include" element. Check included file and
171 // embed the document content inside this documentation node.
173 bool HandleInclude (MemberCore mc, XmlElement el)
175 bool keep_include_node = false;
176 string file = el.GetAttribute ("file");
177 string path = el.GetAttribute ("path");
180 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
181 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
182 keep_include_node = true;
183 } else if (path.Length == 0) {
184 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
185 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
186 keep_include_node = true;
189 Exception exception = null;
190 var full_path = Path.Combine (Path.GetDirectoryName (mc.Location.NameFullPath), file);
192 if (!StoredDocuments.TryGetValue (full_path, out doc)) {
194 doc = new XmlDocument ();
195 doc.Load (full_path);
196 StoredDocuments.Add (full_path, doc);
197 } catch (Exception e) {
199 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
205 XmlNodeList nl = doc.SelectNodes (path);
207 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
209 keep_include_node = true;
211 foreach (XmlNode n in nl)
212 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
213 } catch (Exception ex) {
215 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
219 if (exception != null) {
220 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}'. {2}",
221 path, file, exception.Message);
225 return keep_include_node;
229 // Handles <see> elements.
231 void HandleSee (MemberCore mc, TypeContainer ds, XmlElement see)
233 HandleXrefCommon (mc, ds, see);
237 // Handles <seealso> elements.
239 void HandleSeeAlso (MemberCore mc, TypeContainer ds, XmlElement seealso)
241 HandleXrefCommon (mc, ds, seealso);
245 // Handles <exception> elements.
247 void HandleException (MemberCore mc, TypeContainer ds, XmlElement seealso)
249 HandleXrefCommon (mc, ds, seealso);
253 // Handles <typeparam /> node
255 static void HandleTypeParam (MemberCore mc, XmlElement node)
257 if (!node.HasAttribute ("name"))
260 string tp_name = node.GetAttribute ("name");
261 if (mc.CurrentTypeParameters != null) {
262 if (mc.CurrentTypeParameters.Find (tp_name) != null)
266 // TODO: CS1710, CS1712
268 mc.Compiler.Report.Warning (1711, 2, mc.Location,
269 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
270 mc.GetSignatureForError (), tp_name);
274 // Handles <typeparamref /> node
276 static void HandleTypeParamRef (MemberCore mc, XmlElement node)
278 if (!node.HasAttribute ("name"))
281 string tp_name = node.GetAttribute ("name");
284 if (member.CurrentTypeParameters != null) {
285 if (member.CurrentTypeParameters.Find (tp_name) != null)
289 member = member.Parent;
290 } while (member != null);
292 mc.Compiler.Report.Warning (1735, 2, mc.Location,
293 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
294 mc.GetSignatureForError (), tp_name);
297 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
300 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
302 var left = ResolveMemberName (context, mn.Left);
303 var ns = left as Namespace;
305 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
307 TypeExpr texpr = left as TypeExpr;
309 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
311 return new TypeExpression (found, Location.Null);
320 // Processes "see" or "seealso" elements from cref attribute.
322 void HandleXrefCommon (MemberCore mc, TypeContainer ds, XmlElement xref)
324 string cref = xref.GetAttribute ("cref");
325 // when, XmlReader, "if (cref == null)"
326 if (!xref.HasAttribute ("cref"))
329 // Nothing to be resolved the reference is marked explicitly
330 if (cref.Length > 2 && cref [1] == ':')
333 // Additional symbols for < and > are allowed for easier XML typing
334 cref = cref.Replace ('{', '<').Replace ('}', '>');
336 var encoding = module.Compiler.Settings.Encoding;
337 var s = new MemoryStream (encoding.GetBytes (cref));
339 var source_file = new CompilationSourceFile (doc_module, mc.Location.SourceFile);
340 var report = new Report (doc_module.Compiler, new NullReportPrinter ());
343 session = new ParserSession () {
344 UseJayGlobalArrays = true
347 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding, session.StreamReaderBuffer);
349 var parser = new CSharpParser (seekable, source_file, report, session);
350 ParsedParameters = null;
352 ParsedBuiltinType = null;
353 ParsedOperator = null;
354 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
355 parser.Lexer.parsing_generic_declaration_doc = true;
357 if (report.Errors > 0) {
358 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
359 mc.GetSignatureForError (), cref);
361 xref.SetAttribute ("cref", "!:" + cref);
366 string prefix = null;
367 FullNamedExpression fne = null;
370 // Try built-in type first because we are using ParsedName as identifier of
371 // member names on built-in types
373 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
374 member = ParsedBuiltinType.Type;
379 if (ParsedName != null || ParsedOperator.HasValue) {
380 TypeSpec type = null;
381 string member_name = null;
383 if (member == null) {
384 if (ParsedOperator.HasValue) {
385 type = mc.CurrentType;
386 } else if (ParsedName.Left != null) {
387 fne = ResolveMemberName (mc, ParsedName.Left);
389 var ns = fne as Namespace;
391 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
400 fne = ResolveMemberName (mc, ParsedName);
402 type = mc.CurrentType;
403 } else if (ParsedParameters == null) {
405 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
406 member_name = Constructor.ConstructorName;
411 type = (TypeSpec) member;
415 if (ParsedParameters != null) {
416 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
418 var context = new DocumentationMemberContext (mc, ParsedName ?? MemberName.Null);
420 foreach (var pp in ParsedParameters) {
421 pp.Resolve (context);
424 mc.Module.Compiler.Report.SetPrinter (old_printer);
429 if (member_name == null)
430 member_name = ParsedOperator.HasValue ?
431 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
433 int parsed_param_count;
434 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
435 parsed_param_count = ParsedParameters.Count - 1;
436 } else if (ParsedParameters != null) {
437 parsed_param_count = ParsedParameters.Count;
439 parsed_param_count = 0;
442 int parameters_match = -1;
444 var members = MemberCache.FindMembers (type, member_name, true);
445 if (members != null) {
446 foreach (var m in members) {
447 if (ParsedName != null && m.Arity != ParsedName.Arity)
450 if (ParsedParameters != null) {
451 IParametersMember pm = m as IParametersMember;
455 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
458 var pm_params = pm.Parameters;
461 for (i = 0; i < parsed_param_count; ++i) {
462 var pparam = ParsedParameters[i];
464 if (i >= pm_params.Count || pparam == null || pparam.TypeSpec == null ||
465 !TypeSpecComparer.Override.IsEqual (pparam.TypeSpec, pm_params.Types[i]) ||
466 (pparam.Modifier & Parameter.Modifier.RefOutMask) != (pm_params.FixedParameters[i].ModFlags & Parameter.Modifier.RefOutMask)) {
468 if (i > parameters_match) {
469 parameters_match = i;
480 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
481 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
482 parameters_match = parsed_param_count + 1;
486 if (parsed_param_count != pm_params.Count)
491 if (member != null) {
492 Report.Warning (419, 3, mc.Location,
493 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
494 cref, member.GetSignatureForError (), m.GetSignatureForError ());
503 // Continue with parent type for nested types
504 if (member == null) {
505 type = type.DeclaringType;
509 } while (type != null);
511 if (member == null && parameters_match >= 0) {
512 for (int i = parameters_match; i < parsed_param_count; ++i) {
513 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
514 (i + 1).ToString (), cref);
517 if (parameters_match == parsed_param_count + 1) {
518 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
524 if (member == null) {
525 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
526 mc.GetSignatureForError (), cref);
528 } else if (member == InternalType.Namespace) {
529 cref = "N:" + fne.GetSignatureForError ();
531 prefix = GetMemberDocHead (member);
532 cref = prefix + member.GetSignatureForDocumentation ();
535 xref.SetAttribute ("cref", cref);
539 // Get a prefix from member type for XML documentation (used
540 // to formalize cref target name).
542 static string GetMemberDocHead (MemberSpec type)
544 if (type is FieldSpec)
546 if (type is MethodSpec)
548 if (type is EventSpec)
550 if (type is PropertySpec)
552 if (type is TypeSpec)
555 throw new NotImplementedException (type.GetType ().ToString ());
559 // Raised (and passed an XmlElement that contains the comment)
560 // when GenerateDocComment is writing documentation expectedly.
562 // FIXME: with a few effort, it could be done with XmlReader,
563 // that means removal of DOM use.
565 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
567 HashSet<string> found_tags = null;
568 foreach (XmlElement pelem in el.SelectNodes ("param")) {
569 string xname = pelem.GetAttribute ("name");
570 if (xname.Length == 0)
571 continue; // really? but MS looks doing so
573 if (found_tags == null) {
574 found_tags = new HashSet<string> ();
577 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
578 Report.Warning (1572, 2, member.Location,
579 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
580 member.GetSignatureForError (), xname);
584 if (found_tags.Contains (xname)) {
585 Report.Warning (1571, 2, member.Location,
586 "XML comment on `{0}' has a duplicate param tag for `{1}'",
587 member.GetSignatureForError (), xname);
591 found_tags.Add (xname);
594 if (found_tags != null) {
595 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
596 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
597 Report.Warning (1573, 4, member.Location,
598 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
599 p.Name, member.GetSignatureForError ());
605 // Outputs XML documentation comment from tokenized comments.
607 public bool OutputDocComment (string asmfilename, string xmlFileName)
609 XmlTextWriter w = null;
611 w = new XmlTextWriter (xmlFileName, null);
613 w.Formatting = Formatting.Indented;
614 w.WriteStartDocument ();
615 w.WriteStartElement ("doc");
616 w.WriteStartElement ("assembly");
617 w.WriteStartElement ("name");
618 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
619 w.WriteEndElement (); // name
620 w.WriteEndElement (); // assembly
621 w.WriteStartElement ("members");
622 XmlCommentOutput = w;
623 module.GenerateDocComment (this);
624 w.WriteFullEndElement (); // members
625 w.WriteEndElement ();
626 w.WriteWhitespace (Environment.NewLine);
627 w.WriteEndDocument ();
629 } catch (Exception ex) {
630 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
640 // Type lookup of documentation references uses context of type where
641 // the reference is used but type parameters from cref value
643 sealed class DocumentationMemberContext : IMemberContext
645 readonly MemberCore host;
646 MemberName contextName;
648 public DocumentationMemberContext (MemberCore host, MemberName contextName)
651 this.contextName = contextName;
654 public TypeSpec CurrentType {
656 return host.CurrentType;
660 public TypeParameters CurrentTypeParameters {
662 return contextName.TypeParameters;
666 public MemberCore CurrentMemberDefinition {
668 return host.CurrentMemberDefinition;
672 public bool IsObsolete {
678 public bool IsUnsafe {
680 return host.IsStatic;
684 public bool IsStatic {
686 return host.IsStatic;
690 public ModuleContainer Module {
696 public string GetSignatureForError ()
698 return host.GetSignatureForError ();
701 public ExtensionMethodCandidates LookupExtensionMethod (TypeSpec extensionType, string name, int arity)
706 public FullNamedExpression LookupNamespaceOrType (string name, int arity, LookupMode mode, Location loc)
709 var tp = CurrentTypeParameters;
711 for (int i = 0; i < tp.Count; ++i) {
713 if (t.Name == name) {
714 t.Type.DeclaredPosition = i;
715 return new TypeParameterExpr (t, loc);
721 return host.Parent.LookupNamespaceOrType (name, arity, mode, loc);
724 public FullNamedExpression LookupNamespaceAlias (string name)
726 throw new NotImplementedException ();
730 class DocumentationParameter
732 public readonly Parameter.Modifier Modifier;
733 public FullNamedExpression Type;
736 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
739 this.Modifier = modifier;
742 public DocumentationParameter (FullNamedExpression type)
747 public TypeSpec TypeSpec {
753 public void Resolve (IMemberContext context)
755 type = Type.ResolveAsType (context);