/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/VisualStudio/Core/Impl/CodeModel/FileCodeModel.cs
829 строк
28 KB
Cyrus Najmabadi
Use file scoped namespaces. (#77854)
27 мар 2025, 09:55
Не верифицирован
27 мар 2025, 09:55
98d41b8
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.Collections; using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.InternalElements; using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.Interop; using Microsoft.VisualStudio.LanguageServices.Implementation.Interop; using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel; /// <summary> /// Implementations of EnvDTE.FileCodeModel for both languages. /// </summary> public sealed partial class FileCodeModel : AbstractCodeModelObject, EnvDTE.FileCodeModel, EnvDTE80.FileCodeModel2, ICodeElementContainer<AbstractCodeElement>, IVBFileCodeModelEvents, ICSCodeModelRefactoring { internal static ComHandle<EnvDTE80.FileCodeModel2, FileCodeModel> Create( CodeModelState state, object? parent, DocumentId documentId, bool isSourceGeneratorOutput, ITextManagerAdapter textManagerAdapter) { // Keep track that code model was accessed. We want to get a sense of how widespread usage of it still is. Logger.Log(FunctionId.CodeModel_FileCodeModel_Create); return new FileCodeModel(state, parent, documentId, isSourceGeneratorOutput, textManagerAdapter).GetComHandle<EnvDTE80.FileCodeModel2, FileCodeModel>(); } private readonly ComHandle<object?, object?> _parentHandle; /// <summary> /// Don't use directly. Instead, call <see cref="GetDocumentId()"/>. /// </summary> private DocumentId? _documentId; private readonly bool _isSourceGeneratedOutput; // Note: these are only valid when the underlying file is being renamed. Do not use. private ProjectId? _incomingProjectId; private string? _incomingFilePath; private Document? _previousDocument; private readonly CleanableWeakComHandleTable<SyntaxNodeKey, EnvDTE.CodeElement> _codeElementTable; // These are used during batching. private bool _batchMode; private List<AbstractKeyedCodeElement>? _batchElements; private Document? _batchDocument; // track state to make sure we open editor only once private int _editCount; private IInvisibleEditor? _invisibleEditor; private SyntaxTree _lastSyntaxTree; private FileCodeModel( CodeModelState state, object? parent, DocumentId documentId, bool isSourceGeneratedOutput, ITextManagerAdapter textManagerAdapter) : base(state) { RoslynDebug.AssertNotNull(documentId); RoslynDebug.AssertNotNull(textManagerAdapter); _parentHandle = new ComHandle<object?, object?>(parent); _documentId = documentId; _isSourceGeneratedOutput = isSourceGeneratedOutput; TextManagerAdapter = textManagerAdapter; _codeElementTable = new CleanableWeakComHandleTable<SyntaxNodeKey, EnvDTE.CodeElement>(state.ThreadingContext); _batchMode = false; _batchDocument = null; _lastSyntaxTree = GetSyntaxTree(); } internal ITextManagerAdapter TextManagerAdapter { get; set; } /// <summary> /// Internally, we store the DocumentId for the document that the FileCodeModel represents. If the underlying file /// is renamed, the DocumentId will become invalid because the Roslyn VS workspace treats file renames as a remove/add pair. /// To work around this, the FileCodeModel is notified when a file rename is about to occur. At that point, the /// <see cref="_documentId"/> field is null'd out and <see cref="_incomingFilePath"/> is set to the name of the new file. /// The next time that a FileCodeModel operation occurs that requires the DocumentId, it will be retrieved from the workspace /// using the <see cref="_incomingFilePath"/>. /// </summary> internal void OnRename(string newFilePath) { Debug.Assert(_editCount == 0, "FileCodeModel have an open edit and the underlying file is being renamed. This is a bug."); RoslynDebug.AssertNotNull(_documentId); _previousDocument = Workspace.CurrentSolution.GetDocument(_documentId); _incomingFilePath = newFilePath; _incomingProjectId = _documentId.ProjectId; _documentId = null; } internal override void Shutdown() { if (_invisibleEditor != null) { // we are shutting down, so do not worry about editCount. We will detach our format tracking from the text // buffer now; if anybody else had an invisible editor open to this file, we wouldn't want our format tracking // to trigger. We can safely do that on a background thread since it's just disconnecting a few event handlers. // We have to defer the shutdown of the invisible editor though as that requires talking to the UI thread. // We don't want to block up file removal on the UI thread since we want that path to stay asynchronous. CodeModelService.DetachFormatTrackingToBuffer(_invisibleEditor.TextBuffer); State.ProjectCodeModelFactory.ScheduleDeferredCleanupTask( cancellationToken => { // Ignore cancellationToken: we always need to call Dispose since it triggers the file save. _ = cancellationToken; _invisibleEditor.Dispose(); }); } base.Shutdown(); } private bool TryGetDocumentId([NotNullWhen(true)] out DocumentId? documentId) { if (_documentId != null) { documentId = _documentId; return true; } documentId = null; // We don't have DocumentId, so try to retrieve it from the workspace. if (_incomingProjectId == null || _incomingFilePath == null) { return false; } var project = this.State.Workspace.CurrentSolution.GetProject(_incomingProjectId); if (project == null) { return false; } documentId = project.Solution.GetDocumentIdsWithFilePath(_incomingFilePath).FirstOrDefault(d => d.ProjectId == project.Id); if (documentId == null) { return false; } _documentId = documentId; _incomingProjectId = null; _incomingFilePath = null; _previousDocument = null; return true; } internal DocumentId GetDocumentId() { if (_documentId != null) { return _documentId; } if (TryGetDocumentId(out var documentId)) { return documentId; } throw Exceptions.ThrowEUnexpected(); } internal void UpdateCodeElementNodeKey(AbstractKeyedCodeElement keyedElement, SyntaxNodeKey oldNodeKey, SyntaxNodeKey newNodeKey) { if (!_codeElementTable.TryGetValue(oldNodeKey, out var codeElement)) { throw new InvalidOperationException($"Could not find {oldNodeKey} in Code Model element table."); } _codeElementTable.Remove(oldNodeKey); var managedElement = ComAggregate.GetManagedObject<AbstractKeyedCodeElement>(codeElement); if (!object.Equals(managedElement, keyedElement)) { throw new InvalidOperationException($"Unexpected failure in Code Model while updating node keys {oldNodeKey} -> {newNodeKey}"); } // If we're updating this element with the same node key as an element that's already in the table, // just remove the old element. The old element will continue to function (through its node key), but // the new element will replace it in the cache. if (_codeElementTable.ContainsKey(newNodeKey)) { _codeElementTable.Remove(newNodeKey); } _codeElementTable.Add(newNodeKey, codeElement); } internal void OnCodeElementCreated(SyntaxNodeKey nodeKey, EnvDTE.CodeElement element) { // If we're updating this element with the same node key as an element that's already in the table, // just remove the old element. The old element will continue to function (through its node key), but // the new element will replace it in the cache. if (_codeElementTable.ContainsKey(nodeKey)) { _codeElementTable.Remove(nodeKey); } _codeElementTable.Add(nodeKey, element); } internal void OnCodeElementDeleted(SyntaxNodeKey nodeKey) => _codeElementTable.Remove(nodeKey); internal T GetOrCreateCodeElement<T>(SyntaxNode node) { var nodeKey = CodeModelService.TryGetNodeKey(node); if (!nodeKey.IsEmpty) { // Since the node already has a key, check to see if a code element already // exists for it. If so, return that element it it's still valid; otherwise, // remove it from the table. if (_codeElementTable.TryGetValue(nodeKey, out var codeElement)) { if (codeElement != null) { var element = ComAggregate.GetManagedObject<AbstractCodeElement>(codeElement); if (element.IsValidNode()) { if (codeElement is T tcodeElement) { return tcodeElement; } throw new InvalidOperationException($"Found a valid code element for {nodeKey}, but it is not of type, {typeof(T).ToString()}"); } } } // Go ahead and remove the nodeKey from the table. At this point, we'll be creating a new one. _codeElementTable.Remove(nodeKey); } return (T)CodeModelService.CreateInternalCodeElement(this.State, this, node); } private void InitializeEditor() { // If this is a source generated file, we can't edit it, so just block this at the very start. // E_FAIL is probably as a good as anything else, is and is also what we use files that go missing // so it's consistent for "this file isn't something you can use." if (_isSourceGeneratedOutput) { throw Exceptions.ThrowEFail(); } _editCount++; if (_editCount == 1) { Debug.Assert(_invisibleEditor == null); _invisibleEditor = Workspace.OpenInvisibleEditor(GetDocumentId()); CodeModelService.AttachFormatTrackingToBuffer(_invisibleEditor.TextBuffer); } } private void ReleaseEditor() { Debug.Assert(_editCount >= 1); _editCount--; if (_editCount == 0) { RoslynDebug.AssertNotNull(_invisibleEditor); CodeModelService.DetachFormatTrackingToBuffer(_invisibleEditor.TextBuffer); _invisibleEditor.Dispose(); _invisibleEditor = null; } } internal void EnsureEditor(Action action) { InitializeEditor(); try { action(); } finally { ReleaseEditor(); } } internal T EnsureEditor<T>(Func<T> action) { InitializeEditor(); try { return action(); } finally { ReleaseEditor(); } } internal void PerformEdit(Func<Document, Document> action) { EnsureEditor(() => { Debug.Assert(_invisibleEditor != null); var document = GetDocument(); var workspace = document.Project.Solution.Workspace; var result = action(document); var formatted = State.ThreadingContext.JoinableTaskFactory.Run(async () => { var formattingOptions = await result.GetSyntaxFormattingOptionsAsync(CancellationToken.None).ConfigureAwait(false); var formatted = await Formatter.FormatAsync(result, Formatter.Annotation, formattingOptions, CancellationToken.None).ConfigureAwait(true); formatted = await Formatter.FormatAsync(formatted, SyntaxAnnotation.ElasticAnnotation, formattingOptions, CancellationToken.None).ConfigureAwait(true); return formatted; }); ApplyChanges(workspace, formatted); }); } internal T PerformEdit<T>(Func<Document, Tuple<T, Document>> action) where T : SyntaxNode { return EnsureEditor(() => { Debug.Assert(_invisibleEditor != null); var document = GetDocument(); var workspace = document.Project.Solution.Workspace; var result = action(document); ApplyChanges(workspace, result.Item2); return result.Item1; }); } private void ApplyChanges(Workspace workspace, Document document) { if (IsBatchOpen) { _batchDocument = document; } else { var applied = workspace.TryApplyChanges(document.Project.Solution); if (!applied) { FatalError.ReportAndPropagate(new Exception("Failed to apply the workspace changes."), ErrorSeverity.Critical); } } } internal Document GetDocument() { if (!TryGetDocument(out var document)) { throw Exceptions.ThrowEFail(); } return document; } internal bool TryGetDocument([NotNullWhen(true)] out Document? document) { if (IsBatchOpen && _batchDocument != null) { document = _batchDocument; return true; } if (!TryGetDocumentId(out _) && _previousDocument != null) { document = _previousDocument; } else if (_isSourceGeneratedOutput) { document = State.ThreadingContext.JoinableTaskFactory.Run( () => Workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(GetDocumentId(), CancellationToken.None).AsTask()); } else { document = Workspace.CurrentSolution.GetDocument(GetDocumentId()); } return document != null; } internal SyntaxTree GetSyntaxTree() { return GetDocument().GetRequiredSyntaxTreeSynchronously(CancellationToken.None); } internal SyntaxNode GetSyntaxRoot() { return GetDocument().GetRequiredSyntaxRootSynchronously(CancellationToken.None); } internal SemanticModel GetSemanticModel() => State.ThreadingContext.JoinableTaskFactory.Run(() => { return GetDocument() .GetRequiredSemanticModelAsync(CancellationToken.None).AsTask(); }); internal CodeGenerationOptions GetDocumentOptions() => State.ThreadingContext.JoinableTaskFactory.Run(() => { return GetDocument() .GetCodeGenerationOptionsAsync(CancellationToken.None).AsTask(); }); internal Compilation GetCompilation() => State.ThreadingContext.JoinableTaskFactory.Run(() => { return GetDocument().Project .GetRequiredCompilationAsync(CancellationToken.None); }); internal ProjectId GetProjectId() => GetDocumentId().ProjectId; internal SyntaxNode LookupNode(SyntaxNodeKey nodeKey) => CodeModelService.LookupNode(nodeKey, GetSyntaxTree()); public EnvDTE.CodeAttribute AddAttribute(string name, string value, object position) { return EnsureEditor(() => { return AddAttribute(GetSyntaxRoot(), name, value, position, target: CodeModelService.AssemblyAttributeString); }); } public EnvDTE.CodeClass AddClass(string name, object position, object bases, object implementedInterfaces, EnvDTE.vsCMAccess access) { return EnsureEditor(() => { return AddClass(GetSyntaxRoot(), name, position, bases, implementedInterfaces, access); }); } public EnvDTE.CodeDelegate AddDelegate(string name, object type, object position, EnvDTE.vsCMAccess access) { return EnsureEditor(() => { return AddDelegate(GetSyntaxRoot(), name, type, position, access); }); } public EnvDTE.CodeEnum AddEnum(string name, object position, object bases, EnvDTE.vsCMAccess access) { return EnsureEditor(() => { return AddEnum(GetSyntaxRoot(), name, position, bases, access); }); } public EnvDTE.CodeFunction AddFunction(string name, EnvDTE.vsCMFunction kind, object type, object position, EnvDTE.vsCMAccess access) => throw Exceptions.ThrowEFail(); public EnvDTE80.CodeImport AddImport(string name, object position, string alias) { return EnsureEditor(() => { return AddImport(GetSyntaxRoot(), name, position, alias); }); } public EnvDTE.CodeInterface AddInterface(string name, object position, object bases, EnvDTE.vsCMAccess access) { return EnsureEditor(() => { return AddInterface(GetSyntaxRoot(), name, position, bases, access); }); } public EnvDTE.CodeNamespace AddNamespace(string name, object position) { return EnsureEditor(() => { return AddNamespace(GetSyntaxRoot(), name, position); }); } public EnvDTE.CodeStruct AddStruct(string name, object position, object bases, object implementedInterfaces, EnvDTE.vsCMAccess access) { return EnsureEditor(() => { return AddStruct(GetSyntaxRoot(), name, position, bases, implementedInterfaces, access); }); } public EnvDTE.CodeVariable AddVariable(string name, object type, object position, EnvDTE.vsCMAccess access) => throw Exceptions.ThrowEFail(); public EnvDTE.CodeElement CodeElementFromPoint(EnvDTE.TextPoint point, EnvDTE.vsCMElement scope) { // Can't use point.AbsoluteCharOffset because it's calculated by the native // implementation in GetAbsoluteOffset (in env\msenv\textmgr\autoutil.cpp) // to only count each newline as a single character. We need to ask for line and // column and calculate the right offset ourselves. See DevDiv2 530496 for details. var position = GetPositionFromTextPoint(point); var result = CodeElementFromPosition(position, scope); if (result == null) { throw Exceptions.ThrowEFail(); } return result; } private int GetPositionFromTextPoint(EnvDTE.TextPoint point) { var lineNumber = point.Line - 1; var column = point.LineCharOffset - 1; var line = GetDocument().GetTextSynchronously(CancellationToken.None).Lines[lineNumber]; var position = line.Start + column; return position; } internal EnvDTE.CodeElement? CodeElementFromPosition(int position, EnvDTE.vsCMElement scope) { var root = GetSyntaxRoot(); var leftToken = root.FindTokenOnLeftOfPosition(position); var rightToken = root.FindTokenOnRightOfPosition(position); // We apply a set of heuristics to determine which member we pick to start searching. var token = leftToken; if (leftToken != rightToken) { if (leftToken.Span.End == position && rightToken.SpanStart == position) { // If both tokens are touching, we prefer identifiers and keywords to // separators. Note that the language doesn't allow both tokens to be a // keyword or identifier. if (SyntaxFactsService.IsReservedOrContextualKeyword(rightToken) || SyntaxFactsService.IsIdentifier(rightToken)) { token = rightToken; } } else if (leftToken.Span.End < position && rightToken.SpanStart <= position) { // If only the right token is touching, we have to use it. token = rightToken; } } // If we ended up using the left token but the position is after that token, // walk up to the first node who's last token is not the leftToken. By doing this, we // ensure that we don't find members when the position is actually between them. // In that case, we should find the enclosing type or namespace. var parent = token.Parent; if (token == leftToken && position > token.Span.End) { while (parent != null) { if (parent.GetLastToken() == token) { parent = parent.Parent; } else { break; } } } var node = parent?.AncestorsAndSelf().FirstOrDefault(n => CodeModelService.MatchesScope(n, scope)); if (node == null) { return null; } if (scope == EnvDTE.vsCMElement.vsCMElementAttribute || scope == EnvDTE.vsCMElement.vsCMElementImportStmt || scope == EnvDTE.vsCMElement.vsCMElementParameter || scope == EnvDTE.vsCMElement.vsCMElementOptionStmt || scope == EnvDTE.vsCMElement.vsCMElementInheritsStmt || scope == EnvDTE.vsCMElement.vsCMElementImplementsStmt || (scope == EnvDTE.vsCMElement.vsCMElementFunction && CodeModelService.IsAccessorNode(node))) { // Attributes, imports, parameters, Option, Inherits and Implements // don't have node keys of their own and won't be included in our // collection of elements. Delegate to the service to create these. return CodeModelService.CreateInternalCodeElement(State, this, node); } return GetOrCreateCodeElement<EnvDTE.CodeElement>(node); } public EnvDTE.CodeElements CodeElements { get { return NamespaceCollection.Create(this.State, this, this, SyntaxNodeKey.Empty); } } public EnvDTE.ProjectItem? Parent { get { return _parentHandle.Object as EnvDTE.ProjectItem; } } public void Remove(object element) { var codeElement = ComAggregate.TryGetManagedObject<AbstractCodeElement>(element); codeElement ??= ComAggregate.TryGetManagedObject<AbstractCodeElement>(this.CodeElements.Item(element)); if (codeElement == null) { throw new ArgumentException(ServicesVSResources.Element_is_not_valid, nameof(element)); } codeElement.Delete(); } int IVBFileCodeModelEvents.StartEdit() { try { InitializeEditor(); if (_editCount == 1) { _batchMode = true; _batchElements = []; } return VSConstants.S_OK; } catch (Exception ex) { return Marshal.GetHRForException(ex); } } int IVBFileCodeModelEvents.EndEdit() { try { if (_editCount == 1) { RoslynDebug.AssertNotNull(_batchElements); RoslynDebug.AssertNotNull(_invisibleEditor); List<ValueTuple<AbstractKeyedCodeElement, SyntaxPath>>? elementAndPaths = null; if (_batchElements.Count > 0) { foreach (var element in _batchElements) { var node = element.LookupNode(); if (node != null) { elementAndPaths ??= []; elementAndPaths.Add(ValueTuple.Create(element, new SyntaxPath(node))); } } } if (_batchDocument != null) { // perform expensive operations at once var newDocument = State.ThreadingContext.JoinableTaskFactory.Run(async () => { var simplifierOptions = await _batchDocument.GetSimplifierOptionsAsync(CancellationToken.None).ConfigureAwait(false); return await Simplifier.ReduceAsync(_batchDocument, Simplifier.Annotation, simplifierOptions, CancellationToken.None).ConfigureAwait(false); }); _batchDocument.Project.Solution.Workspace.TryApplyChanges(newDocument.Project.Solution); // done using batch document _batchDocument = null; } // Ensure the file is prettylisted, even if we didn't make any edits CodeModelService.EnsureBufferFormatted(_invisibleEditor.TextBuffer); if (elementAndPaths != null) { foreach (var elementAndPath in elementAndPaths) { // make sure the element is there. if (_codeElementTable.TryGetValue(elementAndPath.Item1.NodeKey, out var existingElement)) { elementAndPath.Item1.ReacquireNodeKey(elementAndPath.Item2, CancellationToken.None); } // make sure existing element doesn't go away (weak reference) in the middle of // updating the node key GC.KeepAlive(existingElement); } } _batchMode = false; _batchElements = null; } return VSConstants.S_OK; } catch (Exception ex) { return Marshal.GetHRForException(ex); } finally { ReleaseEditor(); } } public void BeginBatch() { IVBFileCodeModelEvents temp = this; ErrorHandler.ThrowOnFailure(temp.StartEdit()); } public void EndBatch() { IVBFileCodeModelEvents temp = this; ErrorHandler.ThrowOnFailure(temp.EndEdit()); } [MemberNotNullWhen(true, nameof(_batchElements))] public bool IsBatchOpen { get { return _batchMode && _editCount > 0; } } public EnvDTE.CodeElement ElementFromID(string id) => throw new NotImplementedException(); public EnvDTE80.vsCMParseStatus ParseStatus { get { var syntaxTree = GetSyntaxTree(); return syntaxTree.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error) ? EnvDTE80.vsCMParseStatus.vsCMParseStatusError : EnvDTE80.vsCMParseStatus.vsCMParseStatusComplete; } } public void Synchronize() => FireEvents(); EnvDTE.CodeElements ICodeElementContainer<AbstractCodeElement>.GetCollection() => CodeElements; internal List<GlobalNodeKey> GetCurrentNodeKeys() { var currentNodeKeys = new List<GlobalNodeKey>(); foreach (var element in _codeElementTable.Values) { var keyedElement = ComAggregate.TryGetManagedObject<AbstractKeyedCodeElement>(element); if (keyedElement == null) { continue; } if (keyedElement.TryLookupNode(out var node)) { var nodeKey = keyedElement.NodeKey; currentNodeKeys.Add(new GlobalNodeKey(nodeKey, new SyntaxPath(node))); } } return currentNodeKeys; } internal void ResetElementKeys(List<GlobalNodeKey> globalNodeKeys) { foreach (var globalNodeKey in globalNodeKeys) { ResetElementKey(globalNodeKey); } } private void ResetElementKey(GlobalNodeKey globalNodeKey) { // Failure to find the element is not an error -- it just means the code // element didn't exist... if (_codeElementTable.TryGetValue(globalNodeKey.NodeKey, out var element)) { var keyedElement = ComAggregate.GetManagedObject<AbstractKeyedCodeElement>(element); keyedElement?.ReacquireNodeKey(globalNodeKey.Path, default); } } }