/// <summary> /// Add an unresolved regular-style import (i.e. condition is false). /// </summary> /// <param name="import"> /// The declaring import element. /// </param> void AddUnresolvedImport(ProjectImportElement import) { if (import == null) { throw new ArgumentNullException(nameof(import)); } Position importStart = import.Location.ToNative(); XmlLocation importLocation = _projectXmlLocator.Inspect(importStart); if (importLocation == null) { return; } XSElement importElement; if (!importLocation.IsElement(out importElement)) { return; } Add( new MSBuildUnresolvedImport(import, importElement) ); }
/// <summary> /// Inspect the specified location in the XML. /// </summary> /// <param name="position"> /// The location's position. /// </param> /// <returns> /// An <see cref="XmlLocation"/> representing the result of the inspection. /// </returns> public XmlLocation Inspect(Position position) { // Internally, we always use 1-based indexing because this is what the System.Xml APIs (and I'd rather keep things simple). position = position.ToOneBased(); XSNode nodeAtPosition = FindNode(position); if (nodeAtPosition == null) { return(null); } // If we're on the (seamless, i.e. overlapping) boundary between 2 nodes, select the next node. if (nodeAtPosition.NextSibling != null) { if (position == nodeAtPosition.Range.End && position == nodeAtPosition.NextSibling.Range.Start) { Serilog.Log.Debug("XmlLocator.Inspect moves to next sibling ({NodeKind} @ {NodeRange} -> {NextSiblingKind} @ {NextSiblingRange}).", nodeAtPosition.Kind, nodeAtPosition.Range, nodeAtPosition.NextSibling.Kind, nodeAtPosition.NextSibling.Range ); nodeAtPosition = nodeAtPosition.NextSibling; } } int absolutePosition = _documentPositions.GetAbsolutePosition(position); XmlLocationFlags flags = ComputeLocationFlags(nodeAtPosition, absolutePosition); XmlLocation inspectionResult = new XmlLocation(position, absolutePosition, nodeAtPosition, flags); return(inspectionResult); }
/// <summary> /// Does the location represent a place where an attribute value can be created or replaced by a completion? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="targetAttribute"> /// The attribute (if any) whose value will be replaced by the completion. /// </param> /// <param name="onElementWithPath"> /// If specified, attribute's element must have the specified path. /// </param> /// <param name="forAttributeNamed"> /// If specified, the attribute must have one of the specified names. /// </param> /// <returns> /// <c>true</c>, if the location represents an attribute whose value can be replaced by a completion; otherwise, <c>false</c>. /// </returns> public static bool CanCompleteAttributeValue(this XmlLocation location, out XSAttribute targetAttribute, XSPath onElementWithPath = null, params string[] forAttributeNamed) { if (location == null) { throw new ArgumentNullException(nameof(location)); } targetAttribute = null; XSAttribute attribute; if (!location.IsAttributeValue(out attribute)) { return(false); } if (onElementWithPath != null && !attribute.HasParentPath(onElementWithPath)) { return(false); } if (forAttributeNamed.Length > 0 && Array.IndexOf(forAttributeNamed, attribute.Name) == -1) { return(false); } targetAttribute = attribute; return(true); }
/// <summary> /// Add a regular-style import. /// </summary> /// <param name="resolvedImports"> /// The resolved imports resulting from the import declaration. /// </param> void AddImport(IEnumerable <ResolvedImport> resolvedImports) { if (resolvedImports == null) { throw new ArgumentNullException(nameof(resolvedImports)); } var importsByImportingElement = resolvedImports.GroupBy(import => import.ImportingElement); foreach (var importsForImportingElement in importsByImportingElement) { Position importStart = importsForImportingElement.Key.Location.ToNative(); XmlLocation importLocation = _projectXmlLocator.Inspect(importStart); if (importLocation == null) { continue; } XSElement importElement; if (!importLocation.IsElement(out importElement)) { continue; } Add( new MSBuildImport(importsForImportingElement.ToArray(), importElement) ); } }
/// <summary> /// Add a target. /// </summary> /// <param name="target"> /// The target's declaring <see cref="ProjectTargetElement"/>. /// </param> void AddTarget(ProjectTargetElement target) { if (target == null) { throw new ArgumentNullException(nameof(target)); } XmlLocation targetLocation = _projectXmlLocator.Inspect( target.Location.ToNative() ); if (targetLocation == null) { return; } XSElement targetElement; if (!targetLocation.IsElement(out targetElement)) { return; } Add( new MSBuildTarget(target, targetElement) ); }
/// <summary> /// Get the <see cref="Range"/> represented by the <see cref="InvalidProjectFileException"/>. /// </summary> /// <param name="invalidProjectFileException"> /// The <see cref="InvalidProjectFileException"/>. /// </param> /// <param name="xmlLocator"> /// The XML locator API (if available). /// </param> /// <returns> /// The <see cref="Range"/>. /// </returns> public static Range GetRange(this InvalidProjectFileException invalidProjectFileException, XmlLocator xmlLocator) { if (invalidProjectFileException == null) { throw new ArgumentNullException(nameof(invalidProjectFileException)); } Position startPosition = new Position( invalidProjectFileException.LineNumber, invalidProjectFileException.ColumnNumber ); // Attempt to use the range of the actual XML that the exception refers to. XmlLocation location = xmlLocator?.Inspect(startPosition); if (location != null) { return(location.Node.Range); } // Otherwise, fall back to using the exception's declared end position... Position endPosition = new Position( invalidProjectFileException.EndLineNumber, invalidProjectFileException.EndColumnNumber ); // ...although it's sometimes less reliable. if (endPosition == Position.Zero) { endPosition = startPosition; } return(new Range(startPosition, endPosition)); }
/// <summary> /// Does the location represent a place where an element can be created or replaced by a completion? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="replaceElement"> /// The element (if any) that will be replaced by the completion. /// </param> /// <param name="parentPath"> /// If specified, the location's node's parent must have the specified <see cref="XSPath"/>. /// </param> /// <returns> /// <c>true</c>, if the location represents an element that can be replaced by completion; otherwise, <c>false</c>. /// </returns> /// <remarks> /// We can replace "<>" and "<<Element />". /// </remarks> public static bool CanCompleteElement(this XmlLocation location, out XSElement replaceElement, XSPath parentPath = null) { if (location == null) { throw new ArgumentNullException(nameof(location)); } replaceElement = null; // Simplest case - we're on whitespace so we can simply insert an element without replacing anything. if (location.IsWhitespace(out XSWhitespace whitespace) && (parentPath == null || whitespace.HasParentPath(parentPath))) { return(true); } XSElement element; if (!location.IsElement(out element)) { return(false); } if (location.IsElementBetweenAttributes()) { return(false); } // Check if we need to perform a substition of the element to be replaced (the common case is simply replacing an existing element or partial element). if (element.IsValid) { // Do we have an invalid parent (e.g. "<<Foo />", which yields invalid element named "" with child element named "Foo")? bool isParentValid = element.ParentElement?.IsValid ?? true; if (!isParentValid) { // We can't handle the case where the parent isn't on the same line as the child (since that's not the case outlined above). if (element.ParentElement.Start.LineNumber != location.Node.Start.LineNumber) { return(false); } // But we *can* handle this case by targeting the "parent" element (since that's the element we're actually after anyway). if (location.Node.Start.ColumnNumber - element.ParentElement.Start.ColumnNumber == 1) { element = element.ParentElement; } } } if (parentPath != null && !element.HasParentPath(parentPath)) { return(false); } replaceElement = element; return(true); }
/// <summary> /// Does the location represent text? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <returns> /// <c>true</c>, if the location represents text content within an element; otherwise, <c>false</c>. /// </returns> public static bool IsText(this XmlLocation location) { if (location == null) { throw new ArgumentNullException(nameof(location)); } return(location.Flags.HasFlag(XmlLocationFlags.Text)); }
/// <summary> /// Does the location represent an element's attributes range (but not a specific attribute)? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <returns> /// <c>true</c>, if the location represents an element; otherwise, <c>false</c>. /// </returns> public static bool IsElementBetweenAttributes(this XmlLocation location) { if (location == null) { throw new ArgumentNullException(nameof(location)); } return(location.IsElement() && location.Flags.HasFlag(XmlLocationFlags.Attributes)); }
/// <summary> /// Does the location represent an element's closing tag? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <returns> /// <c>true</c>, if the location represents an element's closing tag; otherwise, <c>false</c>. /// </returns> public static bool IsElementClosingTag(this XmlLocation location) { if (location == null) { throw new ArgumentNullException(nameof(location)); } return(location.IsElement() && location.Flags.HasFlag(XmlLocationFlags.ClosingTag)); }
/// <summary> /// Does the location represent an element's textual content? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <returns> /// <c>true</c>, if the location represents an element's textual content; otherwise, <c>false</c>. /// </returns> public static bool IsElementText(this XmlLocation location) { if (location == null) { throw new ArgumentNullException(nameof(location)); } return(location.IsElement() && location.IsText()); }
/// <summary> /// Does the location represent an element or an attribute? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <returns> /// <c>true</c>, if the location represents an element or attribute; otherwise, <c>false</c>. /// </returns> public static bool IsElementOrAttribute(this XmlLocation location) { if (location == null) { throw new ArgumentNullException(nameof(location)); } return(location.IsElement() || location.IsAttribute()); }
/// <summary> /// Does the location represent an MSBuild expression? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="expression"> /// Receives the expression (if any) at the location. /// </param> /// <param name="expressionRange"> /// The <see cref="Range"/> that contains the expression. /// </param> /// <returns> /// <c>true</c>, if the location represents an MSBuild expression; otherwise, <c>false</c>. /// </returns> public static bool IsExpression(this XmlLocation location, out ExpressionNode expression, out Range expressionRange) { if (location == null) { throw new ArgumentNullException(nameof(location)); } expression = null; expressionRange = Range.Zero; string expressionText; Position expressionStartPosition; if (location.IsElementText(out XSElementText text)) { expressionText = text.Text; expressionStartPosition = text.Range.Start; } else if (location.IsAttributeValue(out XSAttribute attribute)) { expressionText = attribute.Value; expressionStartPosition = attribute.ValueRange.Start; } else if (location.IsWhitespace(out XSWhitespace whitespace)) { expressionText = String.Empty; expressionStartPosition = whitespace.Range.Start; } else { return(false); } ExpressionTree expressionTree; if (!MSBuildExpression.TryParse(expressionText, out expressionTree)) { return(false); } Position expressionPosition = location.Position.RelativeTo(expressionStartPosition); ExpressionNode expressionAtPosition = expressionTree.FindDeepestNodeAt(expressionPosition); if (expressionAtPosition == null) { return(false); } expression = expressionAtPosition; expressionRange = expressionAtPosition.Range.WithOrigin(expressionStartPosition); return(true); }
/// <summary> /// Does the location represent an element that has the specified attribute? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="attributeName"> /// The attribute name. /// </param> /// <param name="attribute"> /// Receives the attribute. /// </param> /// <returns> /// <c>true</c>, if the location represents an element with the specified attribute; otherwise, <c>false</c>. /// </returns> public static bool HasAttribute(this XmlLocation location, string attributeName, out XSAttribute attribute) { if (location.IsElement(out XSElement element)) { attribute = element[attributeName]; return(attribute != null); } attribute = null; return(false); }
/// <summary> /// Does the location represent an attribute's value? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="attribute"> /// Receives the attribute whose value is represented by the location. /// </param> /// <returns> /// <c>true</c>, if the location represents an attribute's name; otherwise, <c>false</c>. /// </returns> public static bool IsAttributeValue(this XmlLocation location, out XSAttribute attribute) { if (location.IsAttributeValue()) { attribute = (XSAttribute)location.Node; return(true); } attribute = null; return(false); }
/// <summary> /// Does the location represent an attribute? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="attribute"> /// Receives the <see cref="XSAttribute"/>. /// </param> /// <returns> /// <c>true</c>, if the location represents an attribute; otherwise, <c>false</c>. /// </returns> public static bool IsAttribute(this XmlLocation location, out XSAttribute attribute) { if (location == null) { throw new ArgumentNullException(nameof(location)); } if (!location.IsAttribute()) { attribute = null; return(false); } attribute = (XSAttribute)location.Node; return(true); }
/// <summary> /// Does the location represent whitespace? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="whitespace"> /// Receives the <see cref="XSWhitespace"/> (if any) at the location. /// </param> /// <returns> /// <c>true</c>, if the location represents whitespace within element content; otherwise, <c>false</c>. /// </returns> public static bool IsWhitespace(this XmlLocation location, out XSWhitespace whitespace) { if (location == null) { throw new ArgumentNullException(nameof(location)); } if (location.IsWhitespace()) { whitespace = (XSWhitespace)location.Node; return(true); } whitespace = null; return(false); }
/// <summary> /// Does the location represent an element's textual content? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="text"> /// Receives the <see cref="XSElementText"/>. /// </param> /// <returns> /// <c>true</c>, if the location represents an element's textual content; otherwise, <c>false</c>. /// </returns> public static bool IsElementText(this XmlLocation location, out XSElementText text) { if (location == null) { throw new ArgumentNullException(nameof(location)); } if (location.IsElementText()) { text = (XSElementText)location.Node; return(true); } else { text = null; return(false); } }
/// <summary> /// Does the location represent an element's attributes range (but not a specific attribute)? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="element"> /// Receives the <see cref="XSElement"/>. /// </param> /// <returns> /// <c>true</c>, if the location represents an element; otherwise, <c>false</c>. /// </returns> public static bool IsElementBetweenAttributes(this XmlLocation location, out XSElement element) { if (location == null) { throw new ArgumentNullException(nameof(location)); } if (location.IsElementBetweenAttributes()) { element = (XSElement)location.Node; return(true); } else { element = null; return(false); } }
/// <summary> /// Add an SDK-style import. /// </summary> /// <param name="resolvedImports"> /// The resolved imports resulting from the import declaration. /// </param> void AddSdkImport(IEnumerable <ResolvedImport> resolvedImports) { if (resolvedImports == null) { throw new ArgumentNullException(nameof(resolvedImports)); } ResolvedImport firstImport = resolvedImports.First(); Position importStart = firstImport.ImportingElement.Location.ToNative(); // If the Sdk attribute is on the Project element rather than an import element, then the location reported by MSBuild will be invalid (go figure). if (importStart == Position.Invalid) { importStart = Position.Origin; } XmlLocation importLocation = _projectXmlLocator.Inspect(importStart); if (importLocation == null) { return; } XSElement importElement; if (!importLocation.IsElement(out importElement)) { return; } XSAttribute sdkAttribute = importElement["Sdk"]; if (sdkAttribute == null) { return; } Add( new MSBuildSdkImport(resolvedImports.ToArray(), sdkAttribute) ); }
/// <summary> /// Add a property. /// </summary> /// <param name="property"> /// The property's declaring <see cref="ProjectPropertyElement"/>. /// </param> void AddProperty(ProjectPropertyElement property) { if (property == null) { throw new ArgumentNullException(nameof(property)); } XmlLocation propertyLocation = _projectXmlLocator.Inspect( property.Location.ToNative() ); if (propertyLocation == null) { return; } XSElement propertyElement; if (!propertyLocation.IsElement(out propertyElement)) { return; } ProjectProperty evaluatedProperty = _project.GetProperty(property.Name); if (evaluatedProperty != null) { Add( new MSBuildProperty(evaluatedProperty, property, propertyElement) ); } else { Add( new MSBuildUnusedProperty(property, propertyElement) ); } }
/// <summary> /// Get the <see cref="Range"/> represented by the <see cref="XmlException"/>. /// </summary> /// <param name="invalidXml"> /// The <see cref="XmlException"/>. /// </param> /// <param name="xmlLocator"> /// The XML locator API (if available). /// </param> /// <returns> /// The <see cref="Range"/>. /// </returns> public static Range GetRange(this XmlException invalidXml, XmlLocator xmlLocator) { if (invalidXml == null) { throw new ArgumentNullException(nameof(invalidXml)); } Position startPosition = new Position( invalidXml.LineNumber, invalidXml.LinePosition ); // Attempt to use the range of the actual XML that the exception refers to. XmlLocation location = xmlLocator?.Inspect(startPosition); if (location != null) { return(location.Node.Range); } // Otherwise, just use the start position. return(startPosition.ToEmptyRange()); }
/// <summary> /// Does the location represent an element that has the specified attribute? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="attributeName"> /// The attribute name. /// </param> /// <returns> /// <c>true</c>, if the location represents an element with the specified attribute; otherwise, <c>false</c>. /// </returns> public static bool HasAttribute(this XmlLocation location, string attributeName) { return(location.IsElement(out XSElement element) && element.HasAttribute(attributeName)); }
/// <summary> /// Add all items defined in the project. /// </summary> void AddItems() { // First, map each item to the element in the XML from where it originates. var itemsByXml = new Dictionary <ProjectItemElement, List <ProjectItem> >(); foreach (ProjectItem item in _project.ItemsIgnoringCondition) { // Must be declared in main project file. if (!IsFromCurrentProject(item.Xml)) { continue; } Position itemStartPosition = item.Xml.Location.ToNative(); List <ProjectItem> itemsFromXml; if (!itemsByXml.TryGetValue(item.Xml, out itemsFromXml)) { itemsFromXml = new List <ProjectItem>(); itemsByXml.Add(item.Xml, itemsFromXml); } itemsFromXml.Add(item); } // Now process item elements and their associated items. HashSet <ProjectItem> usedItems = new HashSet <ProjectItem>(_project.Items); foreach (ProjectItemElement itemXml in itemsByXml.Keys) { Position itemStart = itemXml.Location.ToNative(); XmlLocation itemLocation = _projectXmlLocator.Inspect(itemStart); if (itemLocation == null) { continue; } XSElement itemElement; if (!itemLocation.IsElement(out itemElement)) { continue; } List <ProjectItem> itemsFromXml; if (!itemsByXml.TryGetValue(itemXml, out itemsFromXml)) // AF: Should not happen. { throw new InvalidOperationException($"Found item XML at {itemLocation.Node.Range} with no corresponding items in the MSBuild project (irrespective of condition)."); } if (usedItems.Contains(itemsFromXml[0])) { Add( new MSBuildItemGroup(itemsByXml[itemXml], itemXml, itemElement) ); } else { Add( new MSBuildUnusedItemGroup(itemsByXml[itemXml], itemXml, itemElement) ); } } }
/// <summary> /// Does the location represent a place where an attribute can be created or replaced by a completion? /// </summary> /// <param name="location"> /// The XML location. /// </param> /// <param name="element"> /// The element whose attribute will be completed. /// </param> /// <param name="replaceAttribute"> /// The attribute (if any) that will be replaced by the completion. /// </param> /// <param name="needsPadding"> /// An <see cref="PaddingType"/> value indicating what sort of padding (if any) is required before / after the attribute. /// </param> /// <param name="onElementWithPath"> /// If specified, the location's element must have the specified path. /// </param> /// <returns> /// <c>true</c>, if the location represents an element that can be replaced by completion; otherwise, <c>false</c>. /// </returns> public static bool CanCompleteAttribute(this XmlLocation location, out XSElement element, out XSAttribute replaceAttribute, out PaddingType needsPadding, XSPath onElementWithPath = null) { if (location == null) { throw new ArgumentNullException(nameof(location)); } replaceAttribute = null; needsPadding = PaddingType.None; XSAttribute attribute; if (location.IsAttribute(out attribute) && !location.IsValue()) { element = attribute.Element; if (location.Position == attribute.Start) { // Since we're on an existing attribute, we'll add a new attribute after it. attribute = null; needsPadding = PaddingType.Trailing; } } else if (location.IsElementBetweenAttributes(out element)) { if (element.Attributes.Count > 0) { // Check if we're directly before an attribute. foreach (XSAttribute currentAttribute in element.Attributes) { if (location.Position == currentAttribute.End) { needsPadding = PaddingType.Leading; } else { continue; } break; } } else if (location.Position == element.NameRange.End) // We're directly after the name. { needsPadding = PaddingType.Leading; } } else if (location.IsElement(out element)) { // Check if we're directly after the name. if (location.Position != element.NameRange.End) { return(false); } needsPadding = PaddingType.Leading; } else { return(false); } if (onElementWithPath != null && !element.Path.Matches(onElementWithPath)) { return(false); } replaceAttribute = attribute; return(true); }