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.
15 using System.Collections.Generic;
24 // Implements XML documentation generation.
26 class DocumentationBuilder
29 // Used to create element which helps well-formedness checking.
31 readonly XmlDocument XmlDocumentation;
33 readonly ModuleContainer module;
36 // The output for XML documentation.
38 XmlWriter XmlCommentOutput;
40 static readonly string line_head = Environment.NewLine + " ";
43 // Stores XmlDocuments that are included in XML documentation.
44 // Keys are included filenames, values are XmlDocuments.
46 Dictionary<string, XmlDocument> StoredDocuments = new Dictionary<string, XmlDocument> ();
48 public DocumentationBuilder (ModuleContainer module)
51 XmlDocumentation = new XmlDocument ();
52 XmlDocumentation.PreserveWhitespace = false;
57 return module.Compiler.Report;
61 public MemberName ParsedName {
65 public List<DocumentationParameter> ParsedParameters {
69 public TypeExpression ParsedBuiltinType {
73 public Operator.OpType? ParsedOperator {
77 XmlNode GetDocCommentNode (MemberCore mc, string name)
79 // FIXME: It could be even optimizable as not
80 // to use XmlDocument. But anyways the nodes
81 // are not kept in memory.
82 XmlDocument doc = XmlDocumentation;
84 XmlElement el = doc.CreateElement ("member");
85 el.SetAttribute ("name", name);
86 string normalized = mc.DocComment;
87 el.InnerXml = normalized;
88 // csc keeps lines as written in the sources
89 // and inserts formatting indentation (which
90 // is different from XmlTextWriter.Formatting
91 // one), but when a start tag contains an
92 // endline, it joins the next line. We don't
93 // have to follow such a hacky behavior.
95 normalized.Split ('\n');
97 for (int i = 0; i < split.Length; i++) {
98 string s = split [i].TrimEnd ();
102 el.InnerXml = line_head + String.Join (
103 line_head, split, 0, j);
105 } catch (Exception ex) {
106 Report.Warning (1570, 1, mc.Location, "XML documentation comment on `{0}' is not well-formed XML markup ({1})",
107 mc.GetSignatureForError (), ex.Message);
109 return doc.CreateComment (String.Format ("FIXME: Invalid documentation markup was found for member {0}", name));
114 // Generates xml doc comments (if any), and if required,
115 // handle warning report.
117 internal void GenerateDocumentationForMember (MemberCore mc)
119 string name = mc.DocCommentHeader + mc.GetSignatureForDocumentation ();
121 XmlNode n = GetDocCommentNode (mc, name);
123 XmlElement el = n as XmlElement;
125 var pm = mc as IParametersMember;
127 CheckParametersComments (mc, pm, el);
130 // FIXME: it could be done with XmlReader
131 XmlNodeList nl = n.SelectNodes (".//include");
133 // It could result in current node removal, so prepare another list to iterate.
134 var al = new List<XmlNode> (nl.Count);
135 foreach (XmlNode inc in nl)
137 foreach (XmlElement inc in al)
138 if (!HandleInclude (mc, inc))
139 inc.ParentNode.RemoveChild (inc);
142 // FIXME: it could be done with XmlReader
143 DeclSpace ds_target = mc as DeclSpace;
144 if (ds_target == null)
145 ds_target = mc.Parent;
147 foreach (XmlElement see in n.SelectNodes (".//see"))
148 HandleSee (mc, ds_target, see);
149 foreach (XmlElement seealso in n.SelectNodes (".//seealso"))
150 HandleSeeAlso (mc, ds_target, seealso);
151 foreach (XmlElement see in n.SelectNodes (".//exception"))
152 HandleException (mc, ds_target, see);
153 foreach (XmlElement node in n.SelectNodes (".//typeparam"))
154 HandleTypeParam (mc, node);
155 foreach (XmlElement node in n.SelectNodes (".//typeparamref"))
156 HandleTypeParamRef (mc, node);
159 n.WriteTo (XmlCommentOutput);
163 // Processes "include" element. Check included file and
164 // embed the document content inside this documentation node.
166 bool HandleInclude (MemberCore mc, XmlElement el)
168 bool keep_include_node = false;
169 string file = el.GetAttribute ("file");
170 string path = el.GetAttribute ("path");
172 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
173 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
174 keep_include_node = true;
176 else if (path.Length == 0) {
177 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
178 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
179 keep_include_node = true;
183 if (!StoredDocuments.TryGetValue (file, out doc)) {
185 doc = new XmlDocument ();
187 StoredDocuments.Add (file, doc);
188 } catch (Exception) {
189 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
190 Report.Warning (1592, 1, mc.Location, "Badly formed XML in included comments file -- `{0}'", file);
195 XmlNodeList nl = doc.SelectNodes (path);
197 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
199 keep_include_node = true;
201 foreach (XmlNode n in nl)
202 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
203 } catch (Exception ex) {
204 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
205 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}' ({2})", path, file, ex.Message);
209 return keep_include_node;
213 // Handles <see> elements.
215 void HandleSee (MemberCore mc, DeclSpace ds, XmlElement see)
217 HandleXrefCommon (mc, ds, see);
221 // Handles <seealso> elements.
223 void HandleSeeAlso (MemberCore mc, DeclSpace ds, XmlElement seealso)
225 HandleXrefCommon (mc, ds, seealso);
229 // Handles <exception> elements.
231 void HandleException (MemberCore mc, DeclSpace ds, XmlElement seealso)
233 HandleXrefCommon (mc, ds, seealso);
237 // Handles <typeparam /> node
239 void HandleTypeParam (MemberCore mc, XmlElement node)
241 if (!node.HasAttribute ("name"))
244 string tp_name = node.GetAttribute ("name");
245 if (mc.CurrentTypeParameters != null) {
246 foreach (var tp in mc.CurrentTypeParameters) {
247 if (tp.Name == tp_name)
252 // TODO: CS1710, CS1712
254 mc.Compiler.Report.Warning (1711, 2, mc.Location,
255 "XML comment on `{0}' has a typeparam name `{1}' but there is no type parameter by that name",
256 mc.GetSignatureForError (), tp_name);
260 // Handles <typeparamref /> node
262 void HandleTypeParamRef (MemberCore mc, XmlElement node)
264 if (!node.HasAttribute ("name"))
267 string tp_name = node.GetAttribute ("name");
270 if (member.CurrentTypeParameters != null) {
271 foreach (var tp in member.CurrentTypeParameters) {
272 if (tp.Name == tp_name)
277 member = member.Parent;
278 } while (member != null);
280 mc.Compiler.Report.Warning (1735, 2, mc.Location,
281 "XML comment on `{0}' has a typeparamref name `{1}' that could not be resolved",
282 mc.GetSignatureForError (), tp_name);
285 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
288 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
290 var left = ResolveMemberName (context, mn.Left);
291 var ns = left as Namespace;
293 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
295 TypeExpr texpr = left as TypeExpr;
297 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
299 return new TypeExpression (found, Location.Null);
308 // Processes "see" or "seealso" elements from cref attribute.
310 void HandleXrefCommon (MemberCore mc, DeclSpace ds, XmlElement xref)
312 string cref = xref.GetAttribute ("cref");
313 // when, XmlReader, "if (cref == null)"
314 if (!xref.HasAttribute ("cref"))
317 // Nothing to be resolved the reference is marked explicitly
318 if (cref.Length > 2 && cref [1] == ':')
321 // Additional symbols for < and > are allowed for easier XML typing
322 cref = cref.Replace ('{', '<').Replace ('}', '>');
324 var encoding = module.Compiler.Settings.Encoding;
325 var s = new MemoryStream (encoding.GetBytes (cref));
326 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding);
328 var source_file = new CompilationSourceFile ("{documentation}", "", 1);
329 var doc_module = new ModuleContainer (module.Compiler);
330 doc_module.DocumentationBuilder = this;
331 source_file.NamespaceContainer = new NamespaceContainer (null, doc_module, null, source_file);
333 Report parse_report = new Report (new NullReportPrinter ());
334 var parser = new CSharpParser (seekable, source_file, parse_report);
335 ParsedParameters = null;
337 ParsedBuiltinType = null;
338 ParsedOperator = null;
339 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
340 parser.Lexer.parsing_generic_declaration_doc = true;
342 if (parse_report.Errors > 0) {
343 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
344 mc.GetSignatureForError (), cref);
346 xref.SetAttribute ("cref", "!:" + cref);
351 string prefix = null;
352 FullNamedExpression fne = null;
355 // Try built-in type first because we are using ParsedName as identifier of
356 // member names on built-in types
358 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
359 member = ParsedBuiltinType.Type;
364 if (ParsedName != null || ParsedOperator.HasValue) {
365 TypeSpec type = null;
366 string member_name = null;
368 if (member == null) {
369 if (ParsedOperator.HasValue) {
370 type = mc.CurrentType;
371 } else if (ParsedName.Left != null) {
372 fne = ResolveMemberName (mc, ParsedName.Left);
374 var ns = fne as Namespace;
376 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
385 fne = ResolveMemberName (mc, ParsedName);
387 type = mc.CurrentType;
388 } else if (ParsedParameters == null) {
390 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
391 member_name = Constructor.ConstructorName;
396 type = (TypeSpec) member;
400 if (ParsedParameters != null) {
401 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
402 foreach (var pp in ParsedParameters) {
405 mc.Module.Compiler.Report.SetPrinter (old_printer);
409 if (member_name == null)
410 member_name = ParsedOperator.HasValue ?
411 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
413 int parsed_param_count;
414 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
415 parsed_param_count = ParsedParameters.Count - 1;
416 } else if (ParsedParameters != null) {
417 parsed_param_count = ParsedParameters.Count;
419 parsed_param_count = 0;
422 int parameters_match = -1;
424 var members = MemberCache.FindMembers (type, member_name, true);
425 if (members != null) {
426 foreach (var m in members) {
427 if (ParsedName != null && m.Arity != ParsedName.Arity)
430 if (ParsedParameters != null) {
431 IParametersMember pm = m as IParametersMember;
435 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
439 for (i = 0; i < parsed_param_count; ++i) {
440 var pparam = ParsedParameters[i];
442 if (i >= pm.Parameters.Count || pparam == null ||
443 pparam.TypeSpec != pm.Parameters.Types[i] ||
444 (pparam.Modifier & Parameter.Modifier.SignatureMask) != (pm.Parameters.FixedParameters[i].ModFlags & Parameter.Modifier.SignatureMask)) {
446 if (i > parameters_match) {
447 parameters_match = i;
458 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
459 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
460 parameters_match = parsed_param_count + 1;
464 if (parsed_param_count != pm.Parameters.Count)
469 if (member != null) {
470 Report.Warning (419, 3, mc.Location,
471 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
472 cref, member.GetSignatureForError (), m.GetSignatureForError ());
481 // Continue with parent type for nested types
482 if (member == null) {
483 type = type.DeclaringType;
487 } while (type != null);
489 if (member == null && parameters_match >= 0) {
490 for (int i = parameters_match; i < parsed_param_count; ++i) {
491 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
492 (i + 1).ToString (), cref);
495 if (parameters_match == parsed_param_count + 1) {
496 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
502 if (member == null) {
503 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
504 mc.GetSignatureForError (), cref);
506 } else if (member == InternalType.Namespace) {
507 cref = "N:" + fne.GetSignatureForError ();
509 prefix = GetMemberDocHead (member);
510 cref = prefix + member.GetSignatureForDocumentation ();
513 xref.SetAttribute ("cref", cref);
517 // Get a prefix from member type for XML documentation (used
518 // to formalize cref target name).
520 static string GetMemberDocHead (MemberSpec type)
522 if (type is FieldSpec)
524 if (type is MethodSpec)
526 if (type is EventSpec)
528 if (type is PropertySpec)
530 if (type is TypeSpec)
533 throw new NotImplementedException (type.GetType ().ToString ());
537 // Raised (and passed an XmlElement that contains the comment)
538 // when GenerateDocComment is writing documentation expectedly.
540 // FIXME: with a few effort, it could be done with XmlReader,
541 // that means removal of DOM use.
543 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
545 HashSet<string> found_tags = null;
546 foreach (XmlElement pelem in el.SelectNodes ("param")) {
547 string xname = pelem.GetAttribute ("name");
548 if (xname.Length == 0)
549 continue; // really? but MS looks doing so
551 if (found_tags == null) {
552 found_tags = new HashSet<string> ();
555 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
556 Report.Warning (1572, 2, member.Location,
557 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
558 member.GetSignatureForError (), xname);
562 if (found_tags.Contains (xname)) {
563 Report.Warning (1571, 2, member.Location,
564 "XML comment on `{0}' has a duplicate param tag for `{1}'",
565 member.GetSignatureForError (), xname);
569 found_tags.Add (xname);
572 if (found_tags != null) {
573 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
574 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
575 Report.Warning (1573, 4, member.Location,
576 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
577 p.Name, member.GetSignatureForError ());
583 // Outputs XML documentation comment from tokenized comments.
585 public bool OutputDocComment (string asmfilename, string xmlFileName)
587 XmlTextWriter w = null;
589 w = new XmlTextWriter (xmlFileName, null);
591 w.Formatting = Formatting.Indented;
592 w.WriteStartDocument ();
593 w.WriteStartElement ("doc");
594 w.WriteStartElement ("assembly");
595 w.WriteStartElement ("name");
596 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
597 w.WriteEndElement (); // name
598 w.WriteEndElement (); // assembly
599 w.WriteStartElement ("members");
600 XmlCommentOutput = w;
601 module.GenerateDocComment (this);
602 w.WriteFullEndElement (); // members
603 w.WriteEndElement ();
604 w.WriteWhitespace (Environment.NewLine);
605 w.WriteEndDocument ();
607 } catch (Exception ex) {
608 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
617 class DocumentationParameter
619 public readonly Parameter.Modifier Modifier;
620 public FullNamedExpression Type;
623 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
626 this.Modifier = modifier;
629 public DocumentationParameter (FullNamedExpression type)
634 public TypeSpec TypeSpec {
640 public void Resolve (IMemberContext context)
642 type = Type.ResolveAsType (context);