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");
179 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
180 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
181 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;
190 if (!StoredDocuments.TryGetValue (file, out doc)) {
192 doc = new XmlDocument ();
194 StoredDocuments.Add (file, doc);
195 } catch (Exception) {
196 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
197 Report.Warning (1592, 1, mc.Location, "Badly formed XML in included comments file -- `{0}'", file);
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) {
211 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
212 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}' ({2})", path, file, ex.Message);
216 return keep_include_node;
220 // Handles <see> elements.
222 void HandleSee (MemberCore mc, TypeContainer ds, XmlElement see)
224 HandleXrefCommon (mc, ds, see);
228 // Handles <seealso> elements.
230 void HandleSeeAlso (MemberCore mc, TypeContainer ds, XmlElement seealso)
232 HandleXrefCommon (mc, ds, seealso);
236 // Handles <exception> elements.
238 void HandleException (MemberCore mc, TypeContainer ds, XmlElement seealso)
240 HandleXrefCommon (mc, ds, seealso);
244 // Handles <typeparam /> node
246 static void HandleTypeParam (MemberCore mc, XmlElement node)
248 if (!node.HasAttribute ("name"))
251 string tp_name = node.GetAttribute ("name");
252 if (mc.CurrentTypeParameters != null) {
253 if (mc.CurrentTypeParameters.Find (tp_name) != null)
257 // TODO: CS1710, CS1712
259 mc.Compiler.Report.Warning (1711, 2, mc.Location,
260 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
261 mc.GetSignatureForError (), tp_name);
265 // Handles <typeparamref /> node
267 static void HandleTypeParamRef (MemberCore mc, XmlElement node)
269 if (!node.HasAttribute ("name"))
272 string tp_name = node.GetAttribute ("name");
275 if (member.CurrentTypeParameters != null) {
276 if (member.CurrentTypeParameters.Find (tp_name) != null)
280 member = member.Parent;
281 } while (member != null);
283 mc.Compiler.Report.Warning (1735, 2, mc.Location,
284 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
285 mc.GetSignatureForError (), tp_name);
288 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
291 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
293 var left = ResolveMemberName (context, mn.Left);
294 var ns = left as Namespace;
296 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
298 TypeExpr texpr = left as TypeExpr;
300 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
302 return new TypeExpression (found, Location.Null);
311 // Processes "see" or "seealso" elements from cref attribute.
313 void HandleXrefCommon (MemberCore mc, TypeContainer ds, XmlElement xref)
315 string cref = xref.GetAttribute ("cref");
316 // when, XmlReader, "if (cref == null)"
317 if (!xref.HasAttribute ("cref"))
320 // Nothing to be resolved the reference is marked explicitly
321 if (cref.Length > 2 && cref [1] == ':')
324 // Additional symbols for < and > are allowed for easier XML typing
325 cref = cref.Replace ('{', '<').Replace ('}', '>');
327 var encoding = module.Compiler.Settings.Encoding;
328 var s = new MemoryStream (encoding.GetBytes (cref));
330 var source_file = new CompilationSourceFile (doc_module, mc.Location.SourceFile);
331 var report = new Report (doc_module.Compiler, new NullReportPrinter ());
334 session = new ParserSession () {
335 UseJayGlobalArrays = true
338 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding, session.StreamReaderBuffer);
340 var parser = new CSharpParser (seekable, source_file, report, session);
341 ParsedParameters = null;
343 ParsedBuiltinType = null;
344 ParsedOperator = null;
345 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
346 parser.Lexer.parsing_generic_declaration_doc = true;
348 if (report.Errors > 0) {
349 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
350 mc.GetSignatureForError (), cref);
352 xref.SetAttribute ("cref", "!:" + cref);
357 string prefix = null;
358 FullNamedExpression fne = null;
361 // Try built-in type first because we are using ParsedName as identifier of
362 // member names on built-in types
364 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
365 member = ParsedBuiltinType.Type;
370 if (ParsedName != null || ParsedOperator.HasValue) {
371 TypeSpec type = null;
372 string member_name = null;
374 if (member == null) {
375 if (ParsedOperator.HasValue) {
376 type = mc.CurrentType;
377 } else if (ParsedName.Left != null) {
378 fne = ResolveMemberName (mc, ParsedName.Left);
380 var ns = fne as Namespace;
382 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
391 fne = ResolveMemberName (mc, ParsedName);
393 type = mc.CurrentType;
394 } else if (ParsedParameters == null) {
396 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
397 member_name = Constructor.ConstructorName;
402 type = (TypeSpec) member;
406 if (ParsedParameters != null) {
407 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
409 var context = new DocumentationMemberContext (mc, ParsedName ?? MemberName.Null);
411 foreach (var pp in ParsedParameters) {
412 pp.Resolve (context);
415 mc.Module.Compiler.Report.SetPrinter (old_printer);
420 if (member_name == null)
421 member_name = ParsedOperator.HasValue ?
422 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
424 int parsed_param_count;
425 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
426 parsed_param_count = ParsedParameters.Count - 1;
427 } else if (ParsedParameters != null) {
428 parsed_param_count = ParsedParameters.Count;
430 parsed_param_count = 0;
433 int parameters_match = -1;
435 var members = MemberCache.FindMembers (type, member_name, true);
436 if (members != null) {
437 foreach (var m in members) {
438 if (ParsedName != null && m.Arity != ParsedName.Arity)
441 if (ParsedParameters != null) {
442 IParametersMember pm = m as IParametersMember;
446 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
449 var pm_params = pm.Parameters;
452 for (i = 0; i < parsed_param_count; ++i) {
453 var pparam = ParsedParameters[i];
455 if (i >= pm_params.Count || pparam == null || pparam.TypeSpec == null ||
456 !TypeSpecComparer.Override.IsEqual (pparam.TypeSpec, pm_params.Types[i]) ||
457 (pparam.Modifier & Parameter.Modifier.RefOutMask) != (pm_params.FixedParameters[i].ModFlags & Parameter.Modifier.RefOutMask)) {
459 if (i > parameters_match) {
460 parameters_match = i;
471 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
472 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
473 parameters_match = parsed_param_count + 1;
477 if (parsed_param_count != pm_params.Count)
482 if (member != null) {
483 Report.Warning (419, 3, mc.Location,
484 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
485 cref, member.GetSignatureForError (), m.GetSignatureForError ());
494 // Continue with parent type for nested types
495 if (member == null) {
496 type = type.DeclaringType;
500 } while (type != null);
502 if (member == null && parameters_match >= 0) {
503 for (int i = parameters_match; i < parsed_param_count; ++i) {
504 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
505 (i + 1).ToString (), cref);
508 if (parameters_match == parsed_param_count + 1) {
509 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
515 if (member == null) {
516 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
517 mc.GetSignatureForError (), cref);
519 } else if (member == InternalType.Namespace) {
520 cref = "N:" + fne.GetSignatureForError ();
522 prefix = GetMemberDocHead (member);
523 cref = prefix + member.GetSignatureForDocumentation ();
526 xref.SetAttribute ("cref", cref);
530 // Get a prefix from member type for XML documentation (used
531 // to formalize cref target name).
533 static string GetMemberDocHead (MemberSpec type)
535 if (type is FieldSpec)
537 if (type is MethodSpec)
539 if (type is EventSpec)
541 if (type is PropertySpec)
543 if (type is TypeSpec)
546 throw new NotImplementedException (type.GetType ().ToString ());
550 // Raised (and passed an XmlElement that contains the comment)
551 // when GenerateDocComment is writing documentation expectedly.
553 // FIXME: with a few effort, it could be done with XmlReader,
554 // that means removal of DOM use.
556 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
558 HashSet<string> found_tags = null;
559 foreach (XmlElement pelem in el.SelectNodes ("param")) {
560 string xname = pelem.GetAttribute ("name");
561 if (xname.Length == 0)
562 continue; // really? but MS looks doing so
564 if (found_tags == null) {
565 found_tags = new HashSet<string> ();
568 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
569 Report.Warning (1572, 2, member.Location,
570 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
571 member.GetSignatureForError (), xname);
575 if (found_tags.Contains (xname)) {
576 Report.Warning (1571, 2, member.Location,
577 "XML comment on `{0}' has a duplicate param tag for `{1}'",
578 member.GetSignatureForError (), xname);
582 found_tags.Add (xname);
585 if (found_tags != null) {
586 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
587 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
588 Report.Warning (1573, 4, member.Location,
589 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
590 p.Name, member.GetSignatureForError ());
596 // Outputs XML documentation comment from tokenized comments.
598 public bool OutputDocComment (string asmfilename, string xmlFileName)
600 XmlTextWriter w = null;
602 w = new XmlTextWriter (xmlFileName, null);
604 w.Formatting = Formatting.Indented;
605 w.WriteStartDocument ();
606 w.WriteStartElement ("doc");
607 w.WriteStartElement ("assembly");
608 w.WriteStartElement ("name");
609 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
610 w.WriteEndElement (); // name
611 w.WriteEndElement (); // assembly
612 w.WriteStartElement ("members");
613 XmlCommentOutput = w;
614 module.GenerateDocComment (this);
615 w.WriteFullEndElement (); // members
616 w.WriteEndElement ();
617 w.WriteWhitespace (Environment.NewLine);
618 w.WriteEndDocument ();
620 } catch (Exception ex) {
621 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
631 // Type lookup of documentation references uses context of type where
632 // the reference is used but type parameters from cref value
634 sealed class DocumentationMemberContext : IMemberContext
636 readonly MemberCore host;
637 MemberName contextName;
639 public DocumentationMemberContext (MemberCore host, MemberName contextName)
642 this.contextName = contextName;
645 public TypeSpec CurrentType {
647 return host.CurrentType;
651 public TypeParameters CurrentTypeParameters {
653 return contextName.TypeParameters;
657 public MemberCore CurrentMemberDefinition {
659 return host.CurrentMemberDefinition;
663 public bool IsObsolete {
669 public bool IsUnsafe {
671 return host.IsStatic;
675 public bool IsStatic {
677 return host.IsStatic;
681 public ModuleContainer Module {
687 public string GetSignatureForError ()
689 return host.GetSignatureForError ();
692 public ExtensionMethodCandidates LookupExtensionMethod (TypeSpec extensionType, string name, int arity)
697 public FullNamedExpression LookupNamespaceOrType (string name, int arity, LookupMode mode, Location loc)
700 var tp = CurrentTypeParameters;
702 for (int i = 0; i < tp.Count; ++i) {
704 if (t.Name == name) {
705 t.Type.DeclaredPosition = i;
706 return new TypeParameterExpr (t, loc);
712 return host.Parent.LookupNamespaceOrType (name, arity, mode, loc);
715 public FullNamedExpression LookupNamespaceAlias (string name)
717 throw new NotImplementedException ();
721 class DocumentationParameter
723 public readonly Parameter.Modifier Modifier;
724 public FullNamedExpression Type;
727 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
730 this.Modifier = modifier;
733 public DocumentationParameter (FullNamedExpression type)
738 public TypeSpec TypeSpec {
744 public void Resolve (IMemberContext context)
746 type = Type.ResolveAsType (context);