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);
155 n.WriteTo (XmlCommentOutput);
159 // Processes "include" element. Check included file and
160 // embed the document content inside this documentation node.
162 bool HandleInclude (MemberCore mc, XmlElement el)
164 bool keep_include_node = false;
165 string file = el.GetAttribute ("file");
166 string path = el.GetAttribute ("path");
168 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `file' attribute");
169 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
170 keep_include_node = true;
172 else if (path.Length == 0) {
173 Report.Warning (1590, 1, mc.Location, "Invalid XML `include' element. Missing `path' attribute");
174 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Include tag is invalid "), el);
175 keep_include_node = true;
179 if (!StoredDocuments.TryGetValue (file, out doc)) {
181 doc = new XmlDocument ();
183 StoredDocuments.Add (file, doc);
184 } catch (Exception) {
185 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (String.Format (" Badly formed XML in at comment file `{0}': cannot be included ", file)), el);
186 Report.Warning (1592, 1, mc.Location, "Badly formed XML in included comments file -- `{0}'", file);
191 XmlNodeList nl = doc.SelectNodes (path);
193 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" No matching elements were found for the include tag embedded here. "), el);
195 keep_include_node = true;
197 foreach (XmlNode n in nl)
198 el.ParentNode.InsertBefore (el.OwnerDocument.ImportNode (n, true), el);
199 } catch (Exception ex) {
200 el.ParentNode.InsertBefore (el.OwnerDocument.CreateComment (" Failed to insert some or all of included XML "), el);
201 Report.Warning (1589, 1, mc.Location, "Unable to include XML fragment `{0}' of file `{1}' ({2})", path, file, ex.Message);
205 return keep_include_node;
209 // Handles <see> elements.
211 void HandleSee (MemberCore mc, DeclSpace ds, XmlElement see)
213 HandleXrefCommon (mc, ds, see);
217 // Handles <seealso> elements.
219 void HandleSeeAlso (MemberCore mc, DeclSpace ds, XmlElement seealso)
221 HandleXrefCommon (mc, ds, seealso);
225 // Handles <exception> elements.
227 void HandleException (MemberCore mc, DeclSpace ds, XmlElement seealso)
229 HandleXrefCommon (mc, ds, seealso);
232 FullNamedExpression ResolveMemberName (IMemberContext context, MemberName mn)
235 return context.LookupNamespaceOrType (mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
237 var left = ResolveMemberName (context, mn.Left);
238 var ns = left as Namespace;
240 return ns.LookupTypeOrNamespace (context, mn.Name, mn.Arity, LookupMode.Probing, Location.Null);
242 TypeExpr texpr = left as TypeExpr;
244 var found = MemberCache.FindNestedType (texpr.Type, ParsedName.Name, ParsedName.Arity);
246 return new TypeExpression (found, Location.Null);
255 // Processes "see" or "seealso" elements from cref attribute.
257 void HandleXrefCommon (MemberCore mc, DeclSpace ds, XmlElement xref)
259 string cref = xref.GetAttribute ("cref");
260 // when, XmlReader, "if (cref == null)"
261 if (!xref.HasAttribute ("cref"))
264 // Nothing to be resolved the reference is marked explicitly
265 if (cref.Length > 2 && cref [1] == ':')
268 // Additional symbols for < and > are allowed for easier XML typing
269 cref = cref.Replace ('{', '<').Replace ('}', '>');
271 var encoding = module.Compiler.Settings.Encoding;
272 var s = new MemoryStream (encoding.GetBytes (cref));
273 SeekableStreamReader seekable = new SeekableStreamReader (s, encoding);
275 var source_file = new CompilationSourceFile ("{documentation}", "", 1);
276 var doc_module = new ModuleContainer (module.Compiler);
277 doc_module.DocumentationBuilder = this;
278 source_file.NamespaceContainer = new NamespaceContainer (null, doc_module, null, source_file);
280 Report parse_report = new Report (new NullReportPrinter ());
281 var parser = new CSharpParser (seekable, source_file, parse_report);
282 ParsedParameters = null;
284 ParsedBuiltinType = null;
285 ParsedOperator = null;
286 parser.Lexer.putback_char = Tokenizer.DocumentationXref;
287 parser.Lexer.parsing_generic_declaration_doc = true;
289 if (parse_report.Errors > 0) {
290 Report.Warning (1584, 1, mc.Location, "XML comment on `{0}' has syntactically incorrect cref attribute `{1}'",
291 mc.GetSignatureForError (), cref);
293 xref.SetAttribute ("cref", "!:" + cref);
298 string prefix = null;
299 FullNamedExpression fne = null;
302 // Try built-in type first because we are using ParsedName as identifier of
303 // member names on built-in types
305 if (ParsedBuiltinType != null && (ParsedParameters == null || ParsedName != null)) {
306 member = ParsedBuiltinType.Type;
311 if (ParsedName != null || ParsedOperator.HasValue) {
312 TypeSpec type = null;
313 string member_name = null;
315 if (member == null) {
316 if (ParsedOperator.HasValue) {
317 type = mc.CurrentType;
318 } else if (ParsedName.Left != null) {
319 fne = ResolveMemberName (mc, ParsedName.Left);
321 var ns = fne as Namespace;
323 fne = ns.LookupTypeOrNamespace (mc, ParsedName.Name, ParsedName.Arity, LookupMode.Probing, Location.Null);
332 fne = ResolveMemberName (mc, ParsedName);
334 type = mc.CurrentType;
335 } else if (ParsedParameters == null) {
337 } else if (fne.Type.MemberDefinition == mc.CurrentType.MemberDefinition) {
338 member_name = Constructor.ConstructorName;
343 type = (TypeSpec) member;
347 if (ParsedParameters != null) {
348 var old_printer = mc.Module.Compiler.Report.SetPrinter (new NullReportPrinter ());
349 foreach (var pp in ParsedParameters) {
352 mc.Module.Compiler.Report.SetPrinter (old_printer);
356 if (member_name == null)
357 member_name = ParsedOperator.HasValue ?
358 Operator.GetMetadataName (ParsedOperator.Value) : ParsedName.Name;
360 int parsed_param_count;
361 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
362 parsed_param_count = ParsedParameters.Count - 1;
363 } else if (ParsedParameters != null) {
364 parsed_param_count = ParsedParameters.Count;
366 parsed_param_count = 0;
369 int parameters_match = -1;
371 var members = MemberCache.FindMembers (type, member_name, true);
372 if (members != null) {
373 foreach (var m in members) {
374 if (ParsedName != null && m.Arity != ParsedName.Arity)
377 if (ParsedParameters != null) {
378 IParametersMember pm = m as IParametersMember;
382 if (m.Kind == MemberKind.Operator && !ParsedOperator.HasValue)
386 for (i = 0; i < parsed_param_count; ++i) {
387 var pparam = ParsedParameters[i];
389 if (i >= pm.Parameters.Count || pparam == null ||
390 pparam.TypeSpec != pm.Parameters.Types[i] ||
391 (pparam.Modifier & Parameter.Modifier.SignatureMask) != (pm.Parameters.FixedParameters[i].ModFlags & Parameter.Modifier.SignatureMask)) {
393 if (i > parameters_match) {
394 parameters_match = i;
405 if (ParsedOperator == Operator.OpType.Explicit || ParsedOperator == Operator.OpType.Implicit) {
406 if (pm.MemberType != ParsedParameters[parsed_param_count].TypeSpec) {
407 parameters_match = parsed_param_count + 1;
411 if (parsed_param_count != pm.Parameters.Count)
416 if (member != null) {
417 Report.Warning (419, 3, mc.Location,
418 "Ambiguous reference in cref attribute `{0}'. Assuming `{1}' but other overloads including `{2}' have also matched",
419 cref, member.GetSignatureForError (), m.GetSignatureForError ());
428 // Continue with parent type for nested types
429 if (member == null) {
430 type = type.DeclaringType;
434 } while (type != null);
436 if (member == null && parameters_match >= 0) {
437 for (int i = parameters_match; i < parsed_param_count; ++i) {
438 Report.Warning (1580, 1, mc.Location, "Invalid type for parameter `{0}' in XML comment cref attribute `{1}'",
439 (i + 1).ToString (), cref);
442 if (parameters_match == parsed_param_count + 1) {
443 Report.Warning (1581, 1, mc.Location, "Invalid return type in XML comment cref attribute `{0}'", cref);
449 if (member == null) {
450 Report.Warning (1574, 1, mc.Location, "XML comment on `{0}' has cref attribute `{1}' that could not be resolved",
451 mc.GetSignatureForError (), cref);
453 } else if (member == InternalType.Namespace) {
454 cref = "N:" + fne.GetSignatureForError ();
456 prefix = GetMemberDocHead (member);
457 cref = prefix + member.GetSignatureForDocumentation ();
460 xref.SetAttribute ("cref", cref);
464 // Get a prefix from member type for XML documentation (used
465 // to formalize cref target name).
467 static string GetMemberDocHead (MemberSpec type)
469 if (type is FieldSpec)
471 if (type is MethodSpec)
473 if (type is EventSpec)
475 if (type is PropertySpec)
477 if (type is TypeSpec)
480 throw new NotImplementedException (type.GetType ().ToString ());
484 // Raised (and passed an XmlElement that contains the comment)
485 // when GenerateDocComment is writing documentation expectedly.
487 // FIXME: with a few effort, it could be done with XmlReader,
488 // that means removal of DOM use.
490 void CheckParametersComments (MemberCore member, IParametersMember paramMember, XmlElement el)
492 HashSet<string> found_tags = null;
493 foreach (XmlElement pelem in el.SelectNodes ("param")) {
494 string xname = pelem.GetAttribute ("name");
495 if (xname.Length == 0)
496 continue; // really? but MS looks doing so
498 if (found_tags == null) {
499 found_tags = new HashSet<string> ();
502 if (xname != "" && paramMember.Parameters.GetParameterIndexByName (xname) < 0) {
503 Report.Warning (1572, 2, member.Location,
504 "XML comment on `{0}' has a param tag for `{1}', but there is no parameter by that name",
505 member.GetSignatureForError (), xname);
509 if (found_tags.Contains (xname)) {
510 Report.Warning (1571, 2, member.Location,
511 "XML comment on `{0}' has a duplicate param tag for `{1}'",
512 member.GetSignatureForError (), xname);
516 found_tags.Add (xname);
519 if (found_tags != null) {
520 foreach (Parameter p in paramMember.Parameters.FixedParameters) {
521 if (!found_tags.Contains (p.Name) && !(p is ArglistParameter))
522 Report.Warning (1573, 4, member.Location,
523 "Parameter `{0}' has no matching param tag in the XML comment for `{1}'",
524 p.Name, member.GetSignatureForError ());
530 // Outputs XML documentation comment from tokenized comments.
532 public bool OutputDocComment (string asmfilename, string xmlFileName)
534 XmlTextWriter w = null;
536 w = new XmlTextWriter (xmlFileName, null);
538 w.Formatting = Formatting.Indented;
539 w.WriteStartDocument ();
540 w.WriteStartElement ("doc");
541 w.WriteStartElement ("assembly");
542 w.WriteStartElement ("name");
543 w.WriteString (Path.GetFileNameWithoutExtension (asmfilename));
544 w.WriteEndElement (); // name
545 w.WriteEndElement (); // assembly
546 w.WriteStartElement ("members");
547 XmlCommentOutput = w;
548 module.GenerateDocComment (this);
549 w.WriteFullEndElement (); // members
550 w.WriteEndElement ();
551 w.WriteWhitespace (Environment.NewLine);
552 w.WriteEndDocument ();
554 } catch (Exception ex) {
555 Report.Error (1569, "Error generating XML documentation file `{0}' (`{1}')", xmlFileName, ex.Message);
564 class DocumentationParameter
566 public readonly Parameter.Modifier Modifier;
567 public FullNamedExpression Type;
570 public DocumentationParameter (Parameter.Modifier modifier, FullNamedExpression type)
573 this.Modifier = modifier;
576 public DocumentationParameter (FullNamedExpression type)
581 public TypeSpec TypeSpec {
587 public void Resolve (IMemberContext context)
589 type = Type.ResolveAsType (context);