/// <summary> /// Returns workspace edits for correcting the casing or capitalization of unknown identifiers. /// </summary> private static IEnumerable <(string, WorkspaceEdit)> UnknownIdCaseSuggestions( this FileContentManager file, CompilationUnit compilation, IReadOnlyCollection <Diagnostic> diagnostics) { (string, WorkspaceEdit) SuggestedIdEdit(string suggestedId, Lsp.Range range) { var edit = new TextEdit { Range = range, NewText = suggestedId }; return($"Replace with \"{suggestedId}\".", file.GetWorkspaceEdit(edit)); } var idSuggestions = from diagnostic in diagnostics where DiagnosticTools.ErrorType(ErrorCode.UnknownIdentifier)(diagnostic) from id in file.IdCaseSuggestions(diagnostic.Range.Start.ToQSharp(), compilation) select SuggestedIdEdit(id, diagnostic.Range); var typeSuggestions = from diagnostic in diagnostics where DiagnosticTools.ErrorType(ErrorCode.UnknownType)(diagnostic) from type in file.TypeCaseSuggestions(diagnostic.Range.Start.ToQSharp(), compilation) select SuggestedIdEdit(type, diagnostic.Range); return(idSuggestions.Concat(typeSuggestions)); }
/// <summary> /// Returns a sequence of suggestions on how errors for ambiguous types and callable in the given diagnostics can be fixed, /// given the file for which those diagnostics were generated and the corresponding compilation. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForAmbiguousIdentifiers (this FileContentManager file, CompilationUnit compilation, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var ambiguousCallables = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousCallable)); var ambiguousTypes = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousType)); if (!ambiguousCallables.Any() && !ambiguousTypes.Any()) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } (string, WorkspaceEdit) SuggestedNameQualification(NonNullable <string> suggestedNS, string id, Position pos) { var edit = new TextEdit { Range = new LSP.Range { Start = pos, End = pos }, NewText = $"{suggestedNS.Value}." }; return($"{suggestedNS.Value}.{id}", file.GetWorkspaceEdit(edit)); } var suggestedIdQualifications = ambiguousCallables.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForIdAtPosition(pos, compilation, out var id) .Select(ns => SuggestedNameQualification(ns, id, pos))); var suggestedTypeQualifications = ambiguousTypes.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForTypeAtPosition(pos, compilation, out var id) .Select(ns => SuggestedNameQualification(ns, id, pos))); return(suggestedIdQualifications.Concat(suggestedTypeQualifications)); }
/// <summary> /// Calls the Q# parser on each fragment, splitting one fragment into several if necessary /// (i.e. modifies the list of given fragments!). /// Fragments for which the code only consists of whitespace are left unchanged (i.e. the Kind remains set to null). /// Adds a suitable error to the returned diagnostics for each fragment that cannot be processed. /// Raises an ArgumentNullException if the given diagnostics or fragments are null. /// </summary> private static IEnumerable <Diagnostic> ParseCode(ref List <CodeFragment> fragments, string filename) { if (fragments == null) { throw new ArgumentNullException(nameof(fragments)); } var processedFragments = new List <CodeFragment>(fragments.Count()); var diagnostics = new List <Diagnostic>(); foreach (var snippet in fragments) { var snippetStart = snippet.GetRange().Start; var outputs = Parsing.ProcessCodeFragment(snippet.Text); for (var outputIndex = 0; outputIndex < outputs.Length; ++outputIndex) { var output = outputs[outputIndex]; var fragmentRange = DiagnosticTools.GetAbsoluteRange(snippetStart, output.Range); var fragment = new CodeFragment( snippet.Indentation, fragmentRange, output.Text.Value, outputIndex == outputs.Length - 1 ? snippet.FollowedBy : CodeFragment.MissingDelimiter, output.Kind); processedFragments.Add(fragment); var checkEnding = true; // if there is already a diagnostic overlapping with the ending, then don't bother checking the ending foreach (var fragmentDiagnostic in output.Diagnostics) { var generated = Diagnostics.Generate(filename, fragmentDiagnostic, fragmentRange.Start); diagnostics.Add(generated); var fragmentEnd = fragment.GetRange().End; var diagnosticGoesUpToFragmentEnd = fragmentEnd.IsWithinRange(generated.Range) || fragmentEnd.Equals(generated.Range.End); if (fragmentDiagnostic.Diagnostic.IsError && diagnosticGoesUpToFragmentEnd) { checkEnding = false; } } if (checkEnding) { diagnostics.AddRange(fragment.CheckFragmentDelimiters(filename)); } } if (outputs.Length == 0) { processedFragments.Add(snippet); // keep empty fragments around (note that the kind is set to null in this case!) } } QsCompilerError.RaiseOnFailure(() => ContextBuilder.VerifyTokenOrdering(processedFragments), "processed fragments are not ordered properly and/or overlap"); fragments = processedFragments; return(diagnostics); }
/// <summary> /// Generates a suitable Diagnostic from the given CompilerDiagnostic returned by the Q# compiler. /// The message range contained in the given CompilerDiagnostic is first converted to a Position object, /// and then added to the given positionOffset if the latter is not null. /// Throws an ArgumentNullException if the Range of the given CompilerDiagnostic is null. /// Throws an ArgumentOutOfRangeException if the contained range contains zero or negative entries, or if its Start is bigger than its End. /// </summary> internal static Diagnostic Generate(string filename, QsCompilerDiagnostic msg, Position positionOffset = null) { if (msg.Range == null) { throw new ArgumentNullException(nameof(msg.Range)); } return(new Diagnostic { Severity = Severity(msg), Code = Code(msg), Source = filename, Message = msg.Message, Range = DiagnosticTools.GetAbsoluteRange(positionOffset, msg.Range) }); }
/// <summary> /// Returns a sequence of suggestions on how deprecated syntax can be updated based on the generated diagnostics, /// and given the file for which those diagnostics were generated. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForDeprecatedSyntax (this FileContentManager file, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var deprecatedUnitTypes = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedUnitType)); var deprecatedNOToperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedNOToperator)); var deprecatedANDoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedANDoperator)); var deprecatedORoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedORoperator)); var deprecatedOpCharacteristics = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedOpCharacteristics)); (string, WorkspaceEdit) ReplaceWith(string text, LSP.Range range) {
/// <summary> /// Returns workspace edits for opening namespaces of unknown identifiers. /// </summary> private static IEnumerable <(string, WorkspaceEdit)> UnknownIdNamespaceSuggestions( this FileContentManager file, CompilationUnit compilation, int lineNr, IReadOnlyCollection <Diagnostic> diagnostics) { var idSuggestions = diagnostics .Where(DiagnosticTools.ErrorType(ErrorCode.UnknownIdentifier)) .SelectMany(d => file.IdNamespaceSuggestions(d.Range.Start.ToQSharp(), compilation, out _)); var typeSuggestions = diagnostics .Where(DiagnosticTools.ErrorType(ErrorCode.UnknownType)) .SelectMany(d => file.TypeNamespaceSuggestions(d.Range.Start.ToQSharp(), compilation, out _)); return(file .OpenDirectiveSuggestions(lineNr, idSuggestions.Concat(typeSuggestions)) .Select(edit => (edit.NewText.Trim().Trim(';'), file.GetWorkspaceEdit(edit)))); }
/// <summary> /// Returns a sequence of suggestions on how errors for ambiguous types and callable in the given diagnostics can be fixed, /// given the file for which those diagnostics were generated and the corresponding compilation. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> AmbiguousIdSuggestions( this FileContentManager file, CompilationUnit compilation, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var ambiguousCallables = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousCallable)); var ambiguousTypes = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.AmbiguousType)); if (!ambiguousCallables.Any() && !ambiguousTypes.Any()) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } (string, WorkspaceEdit)? SuggestedNameQualification(string suggestedNS, string?id, Position pos) { var edit = new TextEdit { Range = new Lsp.Range { Start = pos.ToLsp(), End = pos.ToLsp() }, NewText = $"{suggestedNS}." }; return(id is null ? null as (string, WorkspaceEdit)? : ($"{suggestedNS}.{id}", file.GetWorkspaceEdit(edit))); } var suggestedIdQualifications = ambiguousCallables .Select(d => d.Range.Start.ToQSharp()) .SelectMany(pos => file .IdNamespaceSuggestions(pos, compilation, out var id) .SelectNotNull(ns => SuggestedNameQualification(ns, id, pos))); var suggestedTypeQualifications = ambiguousTypes .Select(d => d.Range.Start.ToQSharp()) .SelectMany(pos => file .TypeNamespaceSuggestions(pos, compilation, out var id) .SelectNotNull(ns => SuggestedNameQualification(ns, id, pos))); return(suggestedIdQualifications.Concat(suggestedTypeQualifications)); }
/// <summary> /// Returns a sequence of suggestions on how errors for unknown types and callable in the given diagnostics can be fixed, /// given the file for which those diagnostics were generated and the corresponding compilation. /// The given line number is used to determine the containing namespace. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForUnknownIdentifiers (this FileContentManager file, CompilationUnit compilation, int lineNr, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var unknownCallables = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownIdentifier)); var unknownTypes = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UnknownType)); if (!unknownCallables.Any() && !unknownTypes.Any()) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var suggestionsForIds = unknownCallables.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForIdAtPosition(pos, compilation, out var _)); var suggestionsForTypes = unknownTypes.Select(d => d.Range.Start) .SelectMany(pos => file.NamespaceSuggestionsForTypeAtPosition(pos, compilation, out var _)); return(file.OpenDirectiveSuggestions(lineNr, suggestionsForIds.Concat(suggestionsForTypes).ToArray()) .Select(edit => (edit.NewText.Trim().Trim(';'), file.GetWorkspaceEdit(edit)))); }
/// <summary> /// Returns a sequence of suggestions for update-and-reassign statements based on the generated diagnostics, /// and given the file for which those diagnostics were generated. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForUpdateAndReassignStatements (this FileContentManager file, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var updateOfArrayItemExprs = diagnostics.Where(DiagnosticTools.ErrorType(ErrorCode.UpdateOfArrayItemExpr)); (string, WorkspaceEdit) SuggestedCopyAndUpdateExpr(CodeFragment fragment) { var exprInfo = Parsing.ProcessUpdateOfArrayItemExpr.Invoke(fragment.Text); // Skip if the statement did not match a pattern for which we can give a code action if (exprInfo == null || (exprInfo.Item1.Line == 1 && exprInfo.Item1.Column == 1)) { return("", null); } // Convert set <identifier>[<index>] = <rhs> to set <identifier> w/= <index> <- <rhs> var rhs = $"{exprInfo.Item3} {Keywords.qsCopyAndUpdateOp.cont} {exprInfo.Item4}"; var outputStr = $"{Keywords.qsValueUpdate.id} {exprInfo.Item2} {Keywords.qsCopyAndUpdateOp.op}= {rhs}"; var fragmentRange = fragment.GetRange(); var edit = new TextEdit { Range = fragmentRange.Copy(), NewText = outputStr }; return("Replace with an update-and-reassign statement.", file.GetWorkspaceEdit(edit)); } return(updateOfArrayItemExprs .Select(d => file?.TryGetFragmentAt(d.Range.Start, out var _, includeEnd: true)) .Where(frag => frag != null) .Select(frag => SuggestedCopyAndUpdateExpr(frag)) .Where(s => s.Item2 != null)); }
/// <summary> /// Returns a sequence of suggestions for replacing ranges over array indices with the corresponding library call, /// provided the corresponding library is referenced. /// Returns an empty enumerable if this is not the case or any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForIndexRange (this FileContentManager file, CompilationUnit compilation, LSP.Range range) { if (file == null || compilation == null || range?.Start == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var indexRangeNamespaces = compilation.GlobalSymbols.NamespacesContainingCallable(BuiltIn.IndexRange.Name); if (!indexRangeNamespaces.Contains(BuiltIn.IndexRange.Namespace)) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var suggestedOpenDir = file.OpenDirectiveSuggestions(range.Start.Line, BuiltIn.IndexRange.Namespace); /// Returns true the given expression is of the form "0 .. Length(args) - 1", /// as well as the range of the entire expression and the argument tuple "(args)" as out parameters. bool IsIndexRange(QsExpression iterExpr, Position offset, out LSP.Range exprRange, out LSP.Range argRange) { if (iterExpr.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .RangeLiteral rangeExpression && iterExpr.Range.IsValue && // iterable expression is a valid range literal rangeExpression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .IntLiteral intLiteralExpression && intLiteralExpression.Item == 0L && // .. starting at 0 .. rangeExpression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .SUB SUBExpression && // .. and ending in subracting .. SUBExpression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .IntLiteral subIntLiteralExpression && subIntLiteralExpression.Item == 1L && // .. 1 from .. SUBExpression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .CallLikeExpression callLikeExression && // .. a call .. callLikeExression.Item1.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .Identifier identifier && // .. to and identifier .. identifier.Item1.Symbol is QsSymbolKind <QsSymbol> .Symbol symName && symName.Item.Value == BuiltIn.Length.Name.Value && // .. "Length" called with .. callLikeExression.Item2.Expression is QsExpressionKind <QsExpression, QsSymbol, QsType> .ValueTuple valueTuple && callLikeExression.Item2.Range.IsValue) // .. a valid argument tuple { exprRange = DiagnosticTools.GetAbsoluteRange(offset, iterExpr.Range.Item); argRange = DiagnosticTools.GetAbsoluteRange(offset, callLikeExression.Item2.Range.Item); return(true); } (exprRange, argRange) = (null, null); return(false); } /// Returns the text edits for replacing an range over the indices with the corresponding library call if the given code fragment is a suitable for-loop intro. /// The returned edits do *not* include an edit for adding the corresponding open-directive if necessary. IEnumerable <TextEdit> IndexRangeEdits(CodeFragment fragment) { if (fragment.Kind is QsFragmentKind.ForLoopIntro forLoopIntro && // todo: in principle we could give these suggestions for any index range IsIndexRange(forLoopIntro.Item2, fragment.GetRange().Start, out var iterExprRange, out var argTupleRange)) { yield return(new TextEdit() { Range = new LSP.Range() { Start = iterExprRange.Start, End = argTupleRange.Start }, NewText = BuiltIn.IndexRange.Name.Value }); yield return(new TextEdit() { Range = new LSP.Range() { Start = argTupleRange.End, End = iterExprRange.End }, NewText = "" }); } } var fragments = file.FragmentsOverlappingWithRange(range); var edits = fragments.SelectMany(IndexRangeEdits); return(edits.Any() ? new[] { ("Use IndexRange to iterate over indices.", file.GetWorkspaceEdit(suggestedOpenDir.Concat(edits).ToArray())) }
/// <summary> /// Returns a sequence of suggestions on how deprecated syntax can be updated based on the generated diagnostics, /// and given the file for which those diagnostics were generated. /// Returns an empty enumerable if any of the given arguments is null. /// </summary> internal static IEnumerable <(string, WorkspaceEdit)> SuggestionsForDeprecatedSyntax (this FileContentManager file, IEnumerable <Diagnostic> diagnostics) { if (file == null || diagnostics == null) { return(Enumerable.Empty <(string, WorkspaceEdit)>()); } var deprecatedUnitTypes = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedUnitType)); var deprecatedNOToperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedNOToperator)); var deprecatedANDoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedANDoperator)); var deprecatedORoperators = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedORoperator)); var deprecatedOpCharacteristics = diagnostics.Where(DiagnosticTools.WarningType(WarningCode.DeprecatedOpCharacteristics)); (string, WorkspaceEdit) ReplaceWith(string text, LSP.Range range) { bool NeedsWs(Char ch) => Char.IsLetterOrDigit(ch) || ch == '_'; if (range?.Start != null && range.End != null) { var beforeEdit = file.GetLine(range.Start.Line).Text.Substring(0, range.Start.Character); var afterEdit = file.GetLine(range.End.Line).Text.Substring(range.End.Character); if (beforeEdit.Any() && NeedsWs(beforeEdit.Last())) { text = $" {text}"; } if (afterEdit.Any() && NeedsWs(afterEdit.First())) { text = $"{text} "; } } var edit = new TextEdit { Range = range?.Copy(), NewText = text }; return($"Replace with \"{text.Trim()}\".", file.GetWorkspaceEdit(edit)); } // update deprecated keywords and operators var suggestionsForUnitType = deprecatedUnitTypes.Select(d => ReplaceWith(Keywords.qsUnit.id, d.Range)); var suggestionsForNOT = deprecatedNOToperators.Select(d => ReplaceWith(Keywords.qsNOTop.op, d.Range)); var suggestionsForAND = deprecatedANDoperators.Select(d => ReplaceWith(Keywords.qsANDop.op, d.Range)); var suggestionsForOR = deprecatedORoperators.Select(d => ReplaceWith(Keywords.qsORop.op, d.Range)); // update deprecated operation characteristics syntax var typeToQs = new ExpressionTypeToQs(new ExpressionToQs()); string CharacteristicsAnnotation(Characteristics c) { typeToQs.onCharacteristicsExpression(SymbolResolution.ResolveCharacteristics(c)); return($"{Keywords.qsCharacteristics.id} {typeToQs.Output}"); } var suggestionsForOpCharacteristics = deprecatedOpCharacteristics.SelectMany(d => { // TODO: TryGetQsSymbolInfo currently only returns information about the inner most leafs rather than all types etc. // Once it returns indeed all types in the fragment, the following code block should be replaced by the commented out code below. var fragment = file.TryGetFragmentAt(d.Range.Start, out var _); IEnumerable <Characteristics> GetCharacteristics(QsTuple <Tuple <QsSymbol, QsType> > argTuple) => SyntaxGenerator.ExtractItems(argTuple).SelectMany(item => item.Item2.ExtractCharacteristics()).Distinct(); var characteristicsInFragment = fragment?.Kind is QsFragmentKind.FunctionDeclaration function ? GetCharacteristics(function.Item2.Argument) : fragment?.Kind is QsFragmentKind.OperationDeclaration operation ? GetCharacteristics(operation.Item2.Argument) : fragment?.Kind is QsFragmentKind.TypeDefinition type ? GetCharacteristics(type.Item2) : Enumerable.Empty <Characteristics>(); //var symbolInfo = file.TryGetQsSymbolInfo(d.Range.Start, false, out var fragment); //var characteristicsInFragment = (symbolInfo?.UsedTypes ?? Enumerable.Empty<QsType>()) // .SelectMany(t => t.ExtractCharacteristics()).Distinct(); var fragmentStart = fragment?.GetRange()?.Start; return(characteristicsInFragment .Where(c => c.Range.IsValue && DiagnosticTools.GetAbsoluteRange(fragmentStart, c.Range.Item).Overlaps(d.Range)) .Select(c => ReplaceWith(CharacteristicsAnnotation(c), d.Range))); }); return(suggestionsForOpCharacteristics.ToArray() .Concat(suggestionsForUnitType) .Concat(suggestionsForNOT) .Concat(suggestionsForAND) .Concat(suggestionsForOR)); }