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
151 foreach (XmlElement see in n.SelectNodes (".//see"))
153 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
154 HandleSeeAlso (mc, seealso);
155 foreach (XmlElement see in n.SelectNodes (".//exception"))
156 HandleException (mc, see);
157 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
158 HandleTypeParam (mc, node);
159 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
160 HandleTypeParamRef (mc, node);
163 n.WriteTo (XmlCommentOutput);
167 // Processes "include" element. Check included file and
168 // embed the document content inside this documentation node.
170 bool HandleInclude (MemberCore mc, XmlElement el)
172 bool keep_include_node = false;
173 string file = el.GetAttribute ("file");
174 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;
180 } else if (path.Length == 0) {
181 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
182 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
183 keep_include_node = true;
186 Exception exception = null;
187 var full_path = Path.Combine (Path.GetDirectoryName (mc.Location.NameFullPath), file);
189 if (!StoredDocuments.TryGetValue (full_path, out doc)) {
191 doc = new XmlDocument ();
192 doc.Load (full_path);
193 StoredDocuments.Add (full_path, doc);
194 } catch (Exception e) {
196 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
202 XmlNodeList nl = doc.SelectNodes (path);
204 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
206 keep_include_node = true;
208 foreach (XmlNode n in nl)
209 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
210 } catch (Exception ex) {
212 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
216 if (exception != null) {
217 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}'. {2}",
218 path, file, exception.Message);
222 return keep_include_node;
226 // Handles <see> elements.
228 void HandleSee (MemberCore mc, XmlElement see)
230 HandleXrefCommon (mc, see);
234 // Handles <seealso> elements.
236 void HandleSeeAlso (MemberCore mc, XmlElement seealso)
238 HandleXrefCommon (mc, seealso);
242 // Handles <exception> elements.
244 void HandleException (MemberCore mc, XmlElement seealso)
246 HandleXrefCommon (mc, seealso);
250 // Handles <typeparam /> node
252 static void HandleTypeParam (MemberCore mc, XmlElement node)
254 if (!node.HasAttribute ("name"))
257 string tp_name = node.GetAttribute ("name");
258 if (mc.CurrentTypeParameters != null) {
259 if (mc.CurrentTypeParameters.Find (tp_name) != null)
263 // TODO: CS1710, CS1712
265 mc.Compiler.Report.Warning (1711, 2, mc.Location,
266 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
267 mc.GetSignatureForError (), tp_name);
271 // Handles <typeparamref /> node
273 static void HandleTypeParamRef (MemberCore mc, XmlElement node)
275 if (!node.HasAttribute ("name"))
278 string tp_name = node.GetAttribute ("name");
281 if (member.CurrentTypeParameters != null) {
282 if (member.CurrentTypeParameters.Find (tp_name) != null)
286 member = member.Parent;
287 } while (member != null);
289 mc.Compiler.Report.Warning (1735, 2, mc.Location,
290 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
291 mc.GetSignatureForError (), tp_name);
294 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
297 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
299 var left = ResolveMemberName (context, mn.Left);
300 var ns = left as NamespaceExpression;
302 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
304 TypeExpr texpr = left as TypeExpr;
306 var found = MemberCache.FindNestedType (texpr.Type, mn.Name, mn.Arity);
308 return new TypeExpression (found, Location.Null);
317 // Processes "see" or "seealso" elements from cref attribute.
319 void HandleXrefCommon (MemberCore mc, XmlElement xref)
321 string cref = xref.GetAttribute ("cref");
322 // when, XmlReader, "if (cref == null)"
323 if (!xref.HasAttribute ("cref"))
326 // Nothing to be resolved the reference is marked explicitly
327 if (cref.Length > 2 && cref [1] == ':')
330 // Additional symbols for < and > are allowed for easier XML typing
331 cref = cref.Replace ('{', '<').Replace ('}', '>');
333 var encoding = module.Compiler.Settings.Encoding;
334 var s = new MemoryStream (encoding.GetBytes (cref));
336 var source_file = new CompilationSourceFile (doc_module, mc.Location.SourceFile);
337 var report = new Report (doc_module.Compiler, new NullReportPrinter ());
340 session = new ParserSession {
341 UseJayGlobalArrays = true
344 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding, session.StreamReaderBuffer);
346 var parser = new CSharpParser (seekable, source_file, report, session);
347 ParsedParameters = null;
349 ParsedBuiltinType = null;
350 ParsedOperator = null;
351 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
352 parser.Lexer.parsing_generic_declaration_doc = true;
354 if (report.Errors > 0) {
355 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
356 mc.GetSignatureForError (), cref);
358 xref.SetAttribute ("cref", "!:" + cref);
363 string prefix = null;
364 FullNamedExpression fne = null;
367 // Try built-in type first because we are using ParsedName as identifier of
368 // member names on built-in types
370 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
371 member = ParsedBuiltinType.Type;
376 if (ParsedName != null || ParsedOperator.HasValue) {
377 TypeSpec type = null;
378 string member_name = null;
380 if (member == null) {
381 if (ParsedOperator.HasValue) {
382 type = mc.CurrentType;
383 } else if (ParsedName.Left != null) {
384 fne = ResolveMemberName (mc, ParsedName.Left);
386 var ns = fne as NamespaceExpression;
388 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
397 fne = ResolveMemberName (mc, ParsedName);
399 type = mc.CurrentType;
400 } else if (ParsedParameters == null) {
402 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
403 member_name = Constructor.ConstructorName;
408 type = (TypeSpec) member;
412 if (ParsedParameters != null) {
413 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
415 var context = new DocumentationMemberContext (mc, ParsedName ?? MemberName.Null);
417 foreach (var pp in ParsedParameters) {
418 pp.Resolve (context);
421 mc.Module.Compiler.Report.SetPrinter (old_printer);
426 if (member_name == null)
427 member_name = ParsedOperator.HasValue ?
428 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
430 int parsed_param_count;
431 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
432 parsed_param_count = ParsedParameters.Count - 1;
433 } else if (ParsedParameters != null) {
434 parsed_param_count = ParsedParameters.Count;
436 parsed_param_count = 0;
439 int parameters_match = -1;
441 var members = MemberCache.FindMembers (type, member_name, true);
442 if (members != null) {
443 foreach (var m in members) {
444 if (ParsedName != null && m.Arity != ParsedName.Arity)
447 if (ParsedParameters != null) {
448 IParametersMember pm = m as IParametersMember;
452 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
455 var pm_params = pm.Parameters;
458 for (i = 0; i < parsed_param_count; ++i) {
459 var pparam = ParsedParameters[i];
461 if (i >= pm_params.Count || pparam == null || pparam.TypeSpec == null ||
462 !TypeSpecComparer.Override.IsEqual (pparam.TypeSpec, pm_params.Types[i]) ||
463 (pparam.Modifier & Parameter.Modifier.RefOutMask) != (pm_params.FixedParameters[i].ModFlags & Parameter.Modifier.RefOutMask)) {
465 if (i > parameters_match) {
466 parameters_match = i;
477 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
478 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
479 parameters_match = parsed_param_count + 1;
483 if (parsed_param_count != pm_params.Count)
488 if (member != null) {
489 Report.Warning (419, 3, mc.Location,
490 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
491 cref, member.GetSignatureForError (), m.GetSignatureForError ());
500 // Continue with parent type for nested types
501 if (member == null) {
502 type = type.DeclaringType;
506 } while (type != null);
508 if (member == null && parameters_match >= 0) {
509 for (int i = parameters_match; i < parsed_param_count; ++i) {
510 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
511 (i + 1).ToString (), cref);
514 if (parameters_match == parsed_param_count + 1) {
515 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
521 if (member == null) {
522 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
523 mc.GetSignatureForError (), cref);
525 } else if (member == InternalType.Namespace) {
526 cref = "N:" + fne.GetSignatureForError ();
528 prefix = GetMemberDocHead (member);
529 cref = prefix + member.GetSignatureForDocumentation ();
532 xref.SetAttribute ("cref", cref);
536 // Get a prefix from member type for XML documentation (used
537 // to formalize cref target name).
539 static string GetMemberDocHead (MemberSpec type)
541 if (type is FieldSpec)
543 if (type is MethodSpec)
545 if (type is EventSpec)
547 if (type is PropertySpec)
549 if (type is TypeSpec)
552 throw new NotImplementedException (type.GetType ().ToString ());
556 // Raised (and passed an XmlElement that contains the comment)
557 // when GenerateDocComment is writing documentation expectedly.
559 // FIXME: with a few effort, it could be done with XmlReader,
560 // that means removal of DOM use.
562 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
564 HashSet<string> found_tags = null;
565 foreach (XmlElement pelem in el.SelectNodes ("param")) {
566 string xname = pelem.GetAttribute ("name");
567 if (xname.Length == 0)
568 continue; // really? but MS looks doing so
570 if (found_tags == null) {
571 found_tags = new HashSet<string> ();
574 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
575 Report.Warning (1572, 2, member.Location,
576 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
577 member.GetSignatureForError (), xname);
581 if (found_tags.Contains (xname)) {
582 Report.Warning (1571, 2, member.Location,
583 "XML comment on `{0}' has a duplicate param tag for `{1}'",
584 member.GetSignatureForError (), xname);
588 found_tags.Add (xname);
591 if (found_tags != null) {
592 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
593 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
594 Report.Warning (1573, 4, member.Location,
595 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
596 p.Name, member.GetSignatureForError ());
602 // Outputs XML documentation comment from tokenized comments.
604 public bool OutputDocComment (string asmfilename, string xmlFileName)
606 XmlTextWriter w = null;
608 w = new XmlTextWriter (xmlFileName, null);
610 w.Formatting = Formatting.Indented;
611 w.WriteStartDocument ();
612 w.WriteStartElement ("doc");
613 w.WriteStartElement ("assembly");
614 w.WriteStartElement ("name");
615 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
616 w.WriteEndElement (); // name
617 w.WriteEndElement (); // assembly
618 w.WriteStartElement ("members");
619 XmlCommentOutput = w;
620 module.GenerateDocComment (this);
621 w.WriteFullEndElement (); // members
622 w.WriteEndElement ();
623 w.WriteWhitespace (Environment.NewLine);
624 w.WriteEndDocument ();
626 } catch (Exception ex) {
627 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
637 // Type lookup of documentation references uses context of type where
638 // the reference is used but type parameters from cref value
640 sealed class DocumentationMemberContext : IMemberContext
642 readonly MemberCore host;
643 MemberName contextName;
645 public DocumentationMemberContext (MemberCore host, MemberName contextName)
648 this.contextName = contextName;
651 public TypeSpec CurrentType {
653 return host.CurrentType;
657 public TypeParameters CurrentTypeParameters {
659 return contextName.TypeParameters;
663 public MemberCore CurrentMemberDefinition {
665 return host.CurrentMemberDefinition;
669 public bool IsObsolete {
675 public bool IsUnsafe {
677 return host.IsStatic;
681 public bool IsStatic {
683 return host.IsStatic;
687 public ModuleContainer Module {
693 public string GetSignatureForError ()
695 return host.GetSignatureForError ();
698 public ExtensionMethodCandidates LookupExtensionMethod (TypeSpec extensionType, string name, int arity)
703 public FullNamedExpression LookupNamespaceOrType (string name, int arity, LookupMode mode, Location loc)
706 var tp = CurrentTypeParameters;
708 for (int i = 0; i < tp.Count; ++i) {
710 if (t.Name == name) {
711 t.Type.DeclaredPosition = i;
712 return new TypeParameterExpr (t, loc);
718 return host.Parent.LookupNamespaceOrType (name, arity, mode, loc);
721 public FullNamedExpression LookupNamespaceAlias (string name)
723 throw new NotImplementedException ();
727 class DocumentationParameter
729 public readonly Parameter.Modifier Modifier;
730 public FullNamedExpression Type;
733 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
736 this.Modifier = modifier;
739 public DocumentationParameter (FullNamedExpression type)
744 public TypeSpec TypeSpec {
750 public void Resolve (IMemberContext context)
752 type = Type.ResolveAsType (context);