Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add project files with includes/excludes and options support #3851

Merged
merged 39 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2d0a360
Draft of project file support for CLI commands
keyboardDrummer Apr 6, 2023
e84f9a4
Add error handling and support for excludes
keyboardDrummer Apr 6, 2023
93a4dfb
Improvements
keyboardDrummer Apr 6, 2023
fbbdd16
Update documentation
keyboardDrummer Apr 6, 2023
c0c0705
Add language server test for project files
keyboardDrummer Apr 6, 2023
2ddb0d4
Server project file test passes
keyboardDrummer Apr 6, 2023
71acc75
Fix test
keyboardDrummer Apr 6, 2023
5e4b694
Run formatter
keyboardDrummer Apr 6, 2023
d89e4c2
Update documentation
keyboardDrummer Apr 6, 2023
642849a
Add multiple project files check
keyboardDrummer Apr 6, 2023
38a0ad3
Test for option override behavior
keyboardDrummer Apr 6, 2023
ad354ef
Add test documentation
keyboardDrummer Apr 6, 2023
cdf0369
Added dafny server test, release notes, and removed TODO
keyboardDrummer Apr 6, 2023
ad7b23f
Fix tests
keyboardDrummer Apr 6, 2023
260fbbf
Fix crash
keyboardDrummer Apr 6, 2023
eb52f49
Run formatter
keyboardDrummer Apr 6, 2023
d8a96dd
Add check against bad types
keyboardDrummer Apr 6, 2023
71a72b9
Code review
keyboardDrummer Apr 7, 2023
b06d4b5
Add a default includes pattern
keyboardDrummer Apr 7, 2023
275b73e
Merge branch 'master' into projectFile
keyboardDrummer Apr 7, 2023
cbbe7e6
Fix tests
keyboardDrummer Apr 7, 2023
3d759e3
Update customBoogie.patch
keyboardDrummer Apr 7, 2023
29d4197
Merge branch 'projectFile' of github.com:keyboardDrummer/dafny into p…
keyboardDrummer Apr 7, 2023
6804d5a
Merge branch 'master' into projectFile
keyboardDrummer Apr 7, 2023
bbfb98f
Fix test exception and remove some annoying debug log lines
keyboardDrummer Apr 10, 2023
f0bf212
Turn off some logging and hopefulyl fix a Windows bug
keyboardDrummer Apr 10, 2023
a783681
Improve documentation
keyboardDrummer Apr 11, 2023
eb8e799
Merge branch 'master' into projectFile
keyboardDrummer Apr 11, 2023
8fbff18
Refactoring
keyboardDrummer Apr 12, 2023
e5acbfc
Fix and test extensions
keyboardDrummer Apr 12, 2023
5f588ab
Add validation to check for options in a project file that don't exist
keyboardDrummer Apr 12, 2023
b30332b
Run formatter
keyboardDrummer Apr 12, 2023
ae3d58e
Add warning about non dafny.toml project files
keyboardDrummer Apr 12, 2023
6f280c1
Ran formatter
keyboardDrummer Apr 12, 2023
f6d3b92
Undo lit tests change
keyboardDrummer Apr 13, 2023
7bb1ba1
Prevent crash when project files aren't file paths
keyboardDrummer Apr 13, 2023
1330d0d
Update documentation and use dfyconfig.toml
keyboardDrummer Apr 14, 2023
8f6da0f
Fix test
keyboardDrummer Apr 14, 2023
6cb8001
Merge branch 'master' into projectFile
robin-aws Apr 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Source/DafnyCore/DafnyCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.7.0" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.Runtime.Numerics" Version="4.3.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.0" />
<PackageReference Include="Boogie.ExecutionEngine" Version="2.16.4" />
<PackageReference Include="Tomlyn" Version="0.16.2" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion Source/DafnyCore/DafnyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public enum QuantifierSyntaxOptions {
public record Options(IDictionary<Option, object> OptionArguments);

public class DafnyOptions : Bpl.CommandLineOptions {

public static DafnyOptions Default = new DafnyOptions();
public ProjectFile ProjectFile { get; set; }
public bool NonGhostsUseHeap => Allocated == 1 || Allocated == 2;
public bool AlwaysUseHeap => Allocated == 2;
public bool CommonHeapUse => Allocated >= 2;
Expand Down
2 changes: 1 addition & 1 deletion Source/DafnyCore/Options/ICommandSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface ICommandSpec {
static ICommandSpec() {
FilesArgument = new("file", r => {
return r.Tokens.Where(t => !string.IsNullOrEmpty(t.Value)).Select(t => new FileInfo(t.Value)).ToList();
}, true, "input files");
}, true, "Dafny input files and/or a Dafny project file");
}

public static Argument<List<FileInfo>> FilesArgument { get; }
Expand Down
90 changes: 90 additions & 0 deletions Source/DafnyCore/Options/ProjectFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using Tomlyn;

namespace Microsoft.Dafny;

public class ProjectFile {
public const string FileName = "dfyconfig.toml";

[IgnoreDataMember]
public Uri Uri { get; set; }
keyboardDrummer marked this conversation as resolved.
Show resolved Hide resolved
public string[] Includes { get; set; }
public string[] Excludes { get; set; }
public Dictionary<string, object> Options { get; set; }

public static ProjectFile Open(Uri uri, TextWriter errorWriter) {
if (Path.GetFileName(uri.LocalPath) != FileName) {
Console.WriteLine($"Warning: only Dafny project files named {FileName} are recognised by the Dafny IDE.");
}
try {
var file = File.Open(uri.LocalPath, FileMode.Open);
var model = Toml.ToModel<ProjectFile>(new StreamReader(file).ReadToEnd(), null, new TomlModelOptions());
model.Uri = uri;
return model;

} catch (IOException e) {
errorWriter.WriteLine(e.Message);
return null;
} catch (TomlException tomlException) {
errorWriter.WriteLine($"The Dafny project file {uri.LocalPath} contains the following errors:");
var regex = new Regex(
@$"\((\d+),(\d+)\) : error : The property `(\w+)` was not found on object type {typeof(ProjectFile).FullName}");
var newMessage = regex.Replace(tomlException.Message,
match =>
$"({match.Groups[1].Value},{match.Groups[2].Value}): the property {match.Groups[3].Value} does not exist.");
Comment on lines +41 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't love having to do this, are going down the road of customizing all kinds of generic TOML errors in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until we update the TOML library to use customized error types, yes.

errorWriter.WriteLine(newMessage);
return null;
}
}

public void AddFilesToOptions(DafnyOptions options) {
var matcher = new Matcher();
foreach (var includeGlob in Includes ?? new[] { "**/*.dfy" }) {
matcher.AddInclude(includeGlob);
}
foreach (var includeGlob in Excludes ?? Enumerable.Empty<string>()) {
matcher.AddExclude(includeGlob);
}

var root = Path.GetDirectoryName(Uri.LocalPath);
var result = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(root!)));
var files = result.Files.Select(f => Path.Combine(root, f.Path));
foreach (var file in files) {
options.AddFile(file);
}
}

public void Validate(IEnumerable<Option> possibleOptions) {
var possibleNames = possibleOptions.Select(o => o.Name).ToHashSet();
foreach (var optionThatDoesNotExist in Options.Where(option => !possibleNames.Contains(option.Key))) {
Console.WriteLine($"Warning: option '{optionThatDoesNotExist.Key}' that was specified in the project file, is not a valid Dafny option.");
}
}

public bool TryGetValue(Option option, TextWriter errorWriter, out object value) {
if (!Options.TryGetValue(option.Name, out value)) {
return false;
}

if (value.GetType() != option.ValueType) {
errorWriter.WriteLine(
$"Error: property '{option.Name}' is of type '{value.GetType().Name}' but should be of type '{option.ValueType.Name}'");
return false;
}

return true;

}
}
57 changes: 51 additions & 6 deletions Source/DafnyDriver/Commands/CommandRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ static CommandRegistry() {
AddCommand(new TranslateCommand());
AddCommand(new FormatCommand());
AddCommand(new MeasureComplexityCommand());
AddCommand(new ServerCommand());
AddCommand(ServerCommand.Instance);
AddCommand(new TestCommand());
AddCommand(new GenerateTestsCommand());
AddCommand(new DeadCodeCommand());
AddCommand(new AuditCommand());

FileArgument = new Argument<FileInfo>("file", "input file");
FileArgument = new Argument<FileInfo>("file", "Dafny input file or Dafny project file");
}

public static Argument<FileInfo> FileArgument { get; }
Expand All @@ -60,7 +60,7 @@ public static ParseArgumentResult Create(string[] arguments) {
dafnyOptions.Environment = "Command-line arguments: " + string.Join(" ", arguments);
dafnyOptions.Options = options;

foreach (var option in Commands.SelectMany(c => c.Options)) {
foreach (var option in AllOptions) {
if (!allowHidden) {
option.IsHidden = false;
}
Expand Down Expand Up @@ -105,24 +105,36 @@ public static ParseArgumentResult Create(string[] arguments) {
rootCommand.AddCommand(command);
}

var failedToProcessFile = false;
void CommandHandler(InvocationContext context) {
wasInvoked = true;
var command = context.ParseResult.CommandResult.Command;
var commandSpec = commandToSpec[command];

var singleFile = context.ParseResult.GetValueForArgument(FileArgument);
if (singleFile != null) {
dafnyOptions.AddFile(singleFile.FullName);
if (!ProcessFile(dafnyOptions, singleFile)) {
failedToProcessFile = true;
return;
}
}
var files = context.ParseResult.GetValueForArgument(ICommandSpec.FilesArgument);
if (files != null) {
foreach (var file in files) {
dafnyOptions.AddFile(file.FullName);
if (!ProcessFile(dafnyOptions, file)) {
failedToProcessFile = true;
return;
}
}
}

foreach (var option in command.Options) {
var value = context.ParseResult.GetValueForOption(option);
var result = context.ParseResult.FindResultFor(option);
keyboardDrummer marked this conversation as resolved.
Show resolved Hide resolved
object projectFileValue = null;
var hasProjectFileValue = dafnyOptions.ProjectFile?.TryGetValue(option, Console.Error, out projectFileValue) ?? false;
var value = result == null && hasProjectFileValue
? projectFileValue
: context.ParseResult.GetValueForOption(option);
options.OptionArguments[option] = value;
dafnyOptions.ApplyBinding(option);
}
Expand All @@ -137,6 +149,11 @@ void CommandHandler(InvocationContext context) {
#pragma warning disable VSTHRD002
var exitCode = builder.Build().InvokeAsync(arguments).Result;
#pragma warning restore VSTHRD002

if (failedToProcessFile) {
return new ParseArgumentFailure(DafnyDriver.CommandLineArgumentsResult.PREPROCESSING_ERROR);
}

if (!wasInvoked) {
if (exitCode == 0) {
return new ParseArgumentFailure(DafnyDriver.CommandLineArgumentsResult.OK_EXIT_EARLY);
Expand All @@ -151,6 +168,34 @@ void CommandHandler(InvocationContext context) {
return new ParseArgumentFailure(DafnyDriver.CommandLineArgumentsResult.PREPROCESSING_ERROR);
}

private static bool ProcessFile(DafnyOptions dafnyOptions, FileInfo singleFile) {
if (Path.GetExtension(singleFile.FullName) == ".toml") {
if (dafnyOptions.ProjectFile != null) {
Console.Error.WriteLine($"Only one project file can be used at a time. Both {dafnyOptions.ProjectFile.Uri.LocalPath} and {singleFile.FullName} were specified");
return false;
}

if (!File.Exists(singleFile.FullName)) {
Console.Error.WriteLine($"Error: file {singleFile.FullName} not found");
return false;
}
var projectFile = ProjectFile.Open(new Uri(singleFile.FullName), Console.Error);
if (projectFile == null) {
return false;
}
projectFile.Validate(AllOptions);
dafnyOptions.ProjectFile = projectFile;
projectFile.AddFilesToOptions(dafnyOptions);
} else {
dafnyOptions.AddFile(singleFile.FullName);
}
return true;
}

private static IEnumerable<Option> AllOptions {
get { return Commands.SelectMany(c => c.Options); }
}

private static CommandLineBuilder AddDeveloperHelp(RootCommand rootCommand, CommandLineBuilder builder) {
var languageDeveloperHelp = new Option<bool>(ToolchainDebuggingHelpName,
"Show help and usage information, including options designed for developing the Dafny language and toolchain.");
Expand Down
15 changes: 15 additions & 0 deletions Source/DafnyLanguageServer.Test/DafnyLanguageServer.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@
<None Update="Various\TestFiles\includesAincludesB.dfy">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ProjectFiles\TestFiles\src\foo.dfy">
robin-aws marked this conversation as resolved.
Show resolved Hide resolved
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ProjectFiles\TestFiles\Nesting\dfyconfig.toml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ProjectFiles\TestFiles\dfyconfig.toml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ProjectFiles\TestFiles\noWarnShadowing.dfy">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ProjectFiles\TestFiles\Nesting\src\foo.dfy">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ void NewServerOptionsAction(LanguageServerOptions options) {

private static void ApplyDefaultOptionValues(DafnyOptions dafnyOptions) {
var testCommand = new System.CommandLine.Command("test");
foreach (var serverOption in new ServerCommand().Options) {
foreach (var serverOption in ServerCommand.Instance.Options) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this necessary?

testCommand.AddOption(serverOption);
}

var result = testCommand.Parse("test");
foreach (var option in new ServerCommand().Options) {
foreach (var option in ServerCommand.Instance.Options) {
if (!dafnyOptions.Options.OptionArguments.ContainsKey(option)) {
var value = result.GetValueForOption(option);

Expand Down
2 changes: 1 addition & 1 deletion Source/DafnyLanguageServer.Test/Lookup/HoverTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.Dafny.LanguageServer.IntegrationTest.Lookup {
public class HoverTest : ClientBasedLanguageServerTest {
protected override async Task SetUp(Action<DafnyOptions> modifyOptions = null) {
void ModifyOptions(DafnyOptions options) {
options.ProverOptions.Add("-proverOpt:SOLVER=noop");
options.ProverOptions.Add("SOLVER=noop");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this always wrong before?

Copy link
Member Author

@keyboardDrummer keyboardDrummer Apr 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so. It produced an exception in the CI log that's how I found it. Guess it didn't break the tests.

modifyOptions?.Invoke(options);
}

Expand Down
33 changes: 33 additions & 0 deletions Source/DafnyLanguageServer.Test/ProjectFiles/ProjectFilesTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Dafny.LanguageServer.IntegrationTest.Extensions;
using Microsoft.Dafny.LanguageServer.IntegrationTest.Util;
using Xunit;

namespace Microsoft.Dafny.LanguageServer.IntegrationTest;

public class ProjectFilesTest : ClientBasedLanguageServerTest {
[Fact]
public async Task ClosestProjectFileIsFound() {
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "ProjectFiles/TestFiles/Nesting/src/foo.dfy");
var source = await File.ReadAllTextAsync(filePath);
var documentItem = CreateTestDocument(source, filePath);
await client.OpenDocumentAndWaitAsync(documentItem, CancellationToken);
var diagnostics = await GetLastDiagnostics(documentItem, CancellationToken);

Assert.Single(diagnostics);
Assert.Equal("Shadowed local-variable name: x", diagnostics[0].Message);
}

[Fact]
public async Task ProjectFileOverridesOptions() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a test case with a project file and explicit files passed on the CLI, especially with duplicates? Especially including a file on the CLI that the project file excludes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added those two tests

await SetUp(options => options.WarnShadowing = true);
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "ProjectFiles/TestFiles/noWarnShadowing.dfy");
var source = await File.ReadAllTextAsync(filePath);
var documentItem = CreateTestDocument(source, filePath);
await client.OpenDocumentAndWaitAsync(documentItem, CancellationToken);
var diagnostics = await GetLastDiagnostics(documentItem, CancellationToken);

Assert.Empty(diagnostics);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
includes = ["src/**/*.dfy"]

[options]
warn-shadowing = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
method Foo() {
var x := 3;
if (true) {
var x := 4;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
includes = ["**/*.dfy"]

[options]
warn-shadowing = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
method Foo() {
var x := 3;
if (true) {
var x := 4;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task LoadReturnsCanceledTaskIfOperationIsCanceled() {
var source = new CancellationTokenSource();
parser.Setup(p => p.Parse(It.IsAny<TextDocumentItem>(), It.IsAny<ErrorReporter>(), It.IsAny<CancellationToken>())).Callback(() => source.Cancel())
.Throws<TaskCanceledException>();
var task = textDocumentLoader.LoadAsync(CreateTestDocument(), source.Token);
var task = textDocumentLoader.LoadAsync(DafnyOptions.Default, CreateTestDocument(), source.Token);
try {
await task;
Assert.Fail("document load was not cancelled");
Expand All @@ -68,7 +68,7 @@ public async Task LoadReturnsCanceledTaskIfOperationIsCanceled() {
public async Task LoadReturnsFaultedTaskIfAnyExceptionOccured() {
parser.Setup(p => p.Parse(It.IsAny<TextDocumentItem>(), It.IsAny<ErrorReporter>(), It.IsAny<CancellationToken>()))
.Throws<InvalidOperationException>();
var task = textDocumentLoader.LoadAsync(CreateTestDocument(), default);
var task = textDocumentLoader.LoadAsync(DafnyOptions.Default, CreateTestDocument(), default);
try {
await task;
Assert.Fail("document load did not fail");
Expand Down
5 changes: 3 additions & 2 deletions Source/DafnyLanguageServer.Test/Various/ExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,12 @@ public IdeState CreateUnloaded(DocumentTextBuffer textDocument, CancellationToke
return loader.CreateUnloaded(textDocument, cancellationToken);
}

public Task<DocumentAfterParsing> LoadAsync(DocumentTextBuffer textDocument, CancellationToken cancellationToken) {
public Task<DocumentAfterParsing> LoadAsync(DafnyOptions options, DocumentTextBuffer textDocument,
CancellationToken cancellationToken) {
if (tests.CrashOnLoad) {
throw new IOException("crash");
}
return loader.LoadAsync(textDocument, cancellationToken);
return loader.LoadAsync(options, textDocument, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private ModuleSymbol ProcessModule(Symbol scope, ModuleDefinition moduleDefiniti
case DatatypeDecl dataTypeDeclaration:
return ProcessDataType(moduleSymbol, dataTypeDeclaration);
default:
logger.LogDebug("encountered unknown top level declaration {Name} of type {Type}", topLevelDeclaration.Name, topLevelDeclaration.GetType());
logger.LogTrace("encountered unknown top level declaration {Name} of type {Type}", topLevelDeclaration.Name, topLevelDeclaration.GetType());
return null;
}
}
Expand Down Expand Up @@ -276,7 +276,7 @@ public void Resolve(Expression bodyExpression) {
}

public override void VisitUnknown(object node, IToken token) {
logger.LogDebug("encountered unknown syntax node of type {NodeType} in {Filename}@({Line},{Column})",
logger.LogTrace("encountered unknown syntax node of type {NodeType} in {Filename}@({Line},{Column})",
node.GetType(), token.GetDocumentFileName(), token.line, token.col);
}

Expand Down
Loading