diff --git a/Source/DafnyCore/DafnyCore.csproj b/Source/DafnyCore/DafnyCore.csproj
index 43713d9ab17..4a8b2f89b56 100644
--- a/Source/DafnyCore/DafnyCore.csproj
+++ b/Source/DafnyCore/DafnyCore.csproj
@@ -27,10 +27,12 @@
+
+
diff --git a/Source/DafnyCore/DafnyOptions.cs b/Source/DafnyCore/DafnyOptions.cs
index 144288058f0..b3e10524fbe 100644
--- a/Source/DafnyCore/DafnyOptions.cs
+++ b/Source/DafnyCore/DafnyOptions.cs
@@ -35,7 +35,8 @@ public enum QuantifierSyntaxOptions {
public record Options(IDictionary
diff --git a/Source/DafnyLanguageServer.Test/DafnyLanguageServerTestBase.cs b/Source/DafnyLanguageServer.Test/DafnyLanguageServerTestBase.cs
index 64e77f043bc..1e7fb744a56 100644
--- a/Source/DafnyLanguageServer.Test/DafnyLanguageServerTestBase.cs
+++ b/Source/DafnyLanguageServer.Test/DafnyLanguageServerTestBase.cs
@@ -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) {
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);
diff --git a/Source/DafnyLanguageServer.Test/Lookup/HoverTest.cs b/Source/DafnyLanguageServer.Test/Lookup/HoverTest.cs
index e9641526d04..faa82646d0c 100644
--- a/Source/DafnyLanguageServer.Test/Lookup/HoverTest.cs
+++ b/Source/DafnyLanguageServer.Test/Lookup/HoverTest.cs
@@ -15,7 +15,7 @@ namespace Microsoft.Dafny.LanguageServer.IntegrationTest.Lookup {
public class HoverTest : ClientBasedLanguageServerTest {
protected override async Task SetUp(Action modifyOptions = null) {
void ModifyOptions(DafnyOptions options) {
- options.ProverOptions.Add("-proverOpt:SOLVER=noop");
+ options.ProverOptions.Add("SOLVER=noop");
modifyOptions?.Invoke(options);
}
diff --git a/Source/DafnyLanguageServer.Test/ProjectFiles/ProjectFilesTest.cs b/Source/DafnyLanguageServer.Test/ProjectFiles/ProjectFilesTest.cs
new file mode 100644
index 00000000000..984a601c35b
--- /dev/null
+++ b/Source/DafnyLanguageServer.Test/ProjectFiles/ProjectFilesTest.cs
@@ -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() {
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/dfyconfig.toml b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/dfyconfig.toml
new file mode 100644
index 00000000000..a450e981c8b
--- /dev/null
+++ b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/dfyconfig.toml
@@ -0,0 +1,4 @@
+includes = ["src/**/*.dfy"]
+
+[options]
+warn-shadowing = true
diff --git a/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/src/foo.dfy b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/src/foo.dfy
new file mode 100644
index 00000000000..49a3a161baa
--- /dev/null
+++ b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/Nesting/src/foo.dfy
@@ -0,0 +1,6 @@
+method Foo() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/dfyconfig.toml b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/dfyconfig.toml
new file mode 100644
index 00000000000..59f848c998a
--- /dev/null
+++ b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/dfyconfig.toml
@@ -0,0 +1,4 @@
+includes = ["**/*.dfy"]
+
+[options]
+warn-shadowing = false
diff --git a/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/noWarnShadowing.dfy b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/noWarnShadowing.dfy
new file mode 100644
index 00000000000..49a3a161baa
--- /dev/null
+++ b/Source/DafnyLanguageServer.Test/ProjectFiles/TestFiles/noWarnShadowing.dfy
@@ -0,0 +1,6 @@
+method Foo() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/Source/DafnyLanguageServer.Test/Unit/TextDocumentLoaderTest.cs b/Source/DafnyLanguageServer.Test/Unit/TextDocumentLoaderTest.cs
index 39ca329e553..41c6bfc0b31 100644
--- a/Source/DafnyLanguageServer.Test/Unit/TextDocumentLoaderTest.cs
+++ b/Source/DafnyLanguageServer.Test/Unit/TextDocumentLoaderTest.cs
@@ -53,7 +53,7 @@ public async Task LoadReturnsCanceledTaskIfOperationIsCanceled() {
var source = new CancellationTokenSource();
parser.Setup(p => p.Parse(It.IsAny(), It.IsAny(), It.IsAny())).Callback(() => source.Cancel())
.Throws();
- 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");
@@ -68,7 +68,7 @@ public async Task LoadReturnsCanceledTaskIfOperationIsCanceled() {
public async Task LoadReturnsFaultedTaskIfAnyExceptionOccured() {
parser.Setup(p => p.Parse(It.IsAny(), It.IsAny(), It.IsAny()))
.Throws();
- var task = textDocumentLoader.LoadAsync(CreateTestDocument(), default);
+ var task = textDocumentLoader.LoadAsync(DafnyOptions.Default, CreateTestDocument(), default);
try {
await task;
Assert.Fail("document load did not fail");
diff --git a/Source/DafnyLanguageServer.Test/Various/ExceptionTests.cs b/Source/DafnyLanguageServer.Test/Various/ExceptionTests.cs
index 56f7e7a004d..f18d1caf969 100644
--- a/Source/DafnyLanguageServer.Test/Various/ExceptionTests.cs
+++ b/Source/DafnyLanguageServer.Test/Various/ExceptionTests.cs
@@ -123,11 +123,12 @@ public IdeState CreateUnloaded(DocumentTextBuffer textDocument, CancellationToke
return loader.CreateUnloaded(textDocument, cancellationToken);
}
- public Task LoadAsync(DocumentTextBuffer textDocument, CancellationToken cancellationToken) {
+ public Task 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);
}
}
}
diff --git a/Source/DafnyLanguageServer/Language/Symbols/DafnyLangSymbolResolver.cs b/Source/DafnyLanguageServer/Language/Symbols/DafnyLangSymbolResolver.cs
index c9ca0677875..8acd9bc90dc 100644
--- a/Source/DafnyLanguageServer/Language/Symbols/DafnyLangSymbolResolver.cs
+++ b/Source/DafnyLanguageServer/Language/Symbols/DafnyLangSymbolResolver.cs
@@ -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;
}
}
@@ -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);
}
diff --git a/Source/DafnyLanguageServer/ServerCommand.cs b/Source/DafnyLanguageServer/ServerCommand.cs
index 35aea9985c7..9a347965d63 100644
--- a/Source/DafnyLanguageServer/ServerCommand.cs
+++ b/Source/DafnyLanguageServer/ServerCommand.cs
@@ -8,6 +8,10 @@
namespace Microsoft.Dafny.LanguageServer;
public class ServerCommand : ICommandSpec {
+ public static readonly ServerCommand Instance = new();
+
+ private ServerCommand() {
+ }
static ServerCommand() {
DafnyOptions.RegisterLegacyBinding(VerifySnapshots, (options, u) => options.VerifySnapshots = (int)u);
@@ -53,8 +57,7 @@ related locations
Concat(ICommandSpec.ResolverOptions);
public Command Create() {
- var command = new Command("server", "Start the Dafny language server");
- return command;
+ return new Command("server", "Start the Dafny language server");
}
public void PostProcess(DafnyOptions dafnyOptions, Options options, InvocationContext context) {
diff --git a/Source/DafnyLanguageServer/Workspace/Compilation.cs b/Source/DafnyLanguageServer/Workspace/Compilation.cs
index 09ecf63f3e8..356e71b8e4f 100644
--- a/Source/DafnyLanguageServer/Workspace/Compilation.cs
+++ b/Source/DafnyLanguageServer/Workspace/Compilation.cs
@@ -52,9 +52,10 @@ public class Compilation {
public Task TranslatedDocument { get; }
public Compilation(IServiceProvider services,
+ DafnyOptions options,
DocumentTextBuffer textBuffer,
VerificationTree? migratedVerificationTree) {
- options = services.GetRequiredService();
+ this.options = options;
logger = services.GetRequiredService>();
documentLoader = services.GetRequiredService();
notificationPublisher = services.GetRequiredService();
@@ -79,7 +80,7 @@ public void Start() {
private async Task ResolveAsync() {
try {
await started.Task;
- var documentAfterParsing = await documentLoader.LoadAsync(TextBuffer, cancellationSource.Token);
+ var documentAfterParsing = await documentLoader.LoadAsync(options, TextBuffer, cancellationSource.Token);
// TODO, let gutter icon publications also used the published CompilationView.
var state = documentAfterParsing.InitialIdeState(options);
@@ -337,14 +338,14 @@ public void CancelPendingUpdates() {
private readonly DafnyOptions options;
public void MarkVerificationStarted() {
- logger.LogDebug("MarkVerificationStarted called");
+ logger.LogTrace("MarkVerificationStarted called");
if (verificationCompleted.Task.IsCompleted) {
verificationCompleted = new TaskCompletionSource();
}
}
public void MarkVerificationFinished() {
- logger.LogDebug("MarkVerificationFinished called");
+ logger.LogTrace("MarkVerificationFinished called");
verificationCompleted.TrySetResult();
}
diff --git a/Source/DafnyLanguageServer/Workspace/DocumentManager.cs b/Source/DafnyLanguageServer/Workspace/DocumentManager.cs
index 80de9a8b72b..e4b1b956cc0 100644
--- a/Source/DafnyLanguageServer/Workspace/DocumentManager.cs
+++ b/Source/DafnyLanguageServer/Workspace/DocumentManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
@@ -10,6 +11,7 @@
using Microsoft.Dafny.LanguageServer.Workspace.Notifications;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
@@ -54,6 +56,7 @@ public DocumentManager(
document);
Compilation = new Compilation(
services,
+ DetermineDocumentOptions(options, document.Uri),
document,
null);
@@ -104,8 +107,11 @@ public void UpdateDocument(DidChangeTextDocumentParams documentChange) {
relocator.RelocateRange(range, documentChange, CancellationToken.None))).
Where(r => r != null).Take(MaxRememberedChanges).ToList()!;
}
+
+ var dafnyOptions = DetermineDocumentOptions(options, documentChange.TextDocument.Uri);
Compilation = new Compilation(
services,
+ dafnyOptions,
updatedText,
// TODO do not pass this to CompilationManager but instead use it in FillMissingStateUsingLastPublishedDocument
migratedVerificationTree
@@ -127,6 +133,40 @@ public void UpdateDocument(DidChangeTextDocumentParams documentChange) {
Compilation.Start();
}
+ private static DafnyOptions DetermineDocumentOptions(DafnyOptions serverOptions, DocumentUri uri) {
+ ProjectFile? projectFile = null;
+
+ var folder = Path.GetDirectoryName(uri.GetFileSystemPath());
+ while (!string.IsNullOrEmpty(folder)) {
+ var children = Directory.GetFiles(folder, "dfyconfig.toml");
+ if (children.Length > 0) {
+ var errorWriter = TextWriter.Null;
+ projectFile = ProjectFile.Open(new Uri(children[0]), errorWriter);
+ if (projectFile != null) {
+ break;
+ }
+ }
+ folder = Path.GetDirectoryName(folder);
+ }
+
+ if (projectFile != null) {
+ var result = new DafnyOptions(serverOptions);
+
+ foreach (var option in ServerCommand.Instance.Options) {
+ object? projectFileValue = null;
+ var hasProjectFileValue = projectFile?.TryGetValue(option, TextWriter.Null, out projectFileValue) ?? false;
+ if (hasProjectFileValue) {
+ result.Options.OptionArguments[option] = projectFileValue;
+ result.ApplyBinding(option);
+ }
+ }
+
+ return result;
+ }
+
+ return serverOptions;
+ }
+
private Dictionary MigrateImplementationViews(DidChangeTextDocumentParams documentChange,
IReadOnlyDictionary oldVerificationDiagnostics) {
var result = new Dictionary();
diff --git a/Source/DafnyLanguageServer/Workspace/DocumentManagerDatabase.cs b/Source/DafnyLanguageServer/Workspace/DocumentManagerDatabase.cs
index 4977992cac9..65026b8702a 100644
--- a/Source/DafnyLanguageServer/Workspace/DocumentManagerDatabase.cs
+++ b/Source/DafnyLanguageServer/Workspace/DocumentManagerDatabase.cs
@@ -18,7 +18,6 @@ public class DocumentManagerDatabase : IDocumentDatabase {
private readonly Dictionary documents = new();
public DocumentManagerDatabase(IServiceProvider services) {
-
this.services = services;
}
diff --git a/Source/DafnyLanguageServer/Workspace/ITextDocumentLoader.cs b/Source/DafnyLanguageServer/Workspace/ITextDocumentLoader.cs
index b66fb55baf1..e7cb0536492 100644
--- a/Source/DafnyLanguageServer/Workspace/ITextDocumentLoader.cs
+++ b/Source/DafnyLanguageServer/Workspace/ITextDocumentLoader.cs
@@ -26,11 +26,10 @@ public interface ITextDocumentLoader {
/// Loads the specified document item of the language server and applies the necessary steps
/// to generate a dafny document instance.
///
- /// The text document to load.
- /// A token to cancel the update operation before its completion.
/// The loaded dafny document.
/// Thrown when the cancellation was requested before completion.
/// Thrown if the cancellation token was disposed before the completion.
- Task LoadAsync(DocumentTextBuffer textDocument, CancellationToken cancellationToken);
+ Task LoadAsync(DafnyOptions options, DocumentTextBuffer textDocument,
+ CancellationToken cancellationToken);
}
}
diff --git a/Source/DafnyLanguageServer/Workspace/IdeStateObserver.cs b/Source/DafnyLanguageServer/Workspace/IdeStateObserver.cs
index 42898c31cbb..3e04f890321 100644
--- a/Source/DafnyLanguageServer/Workspace/IdeStateObserver.cs
+++ b/Source/DafnyLanguageServer/Workspace/IdeStateObserver.cs
@@ -56,7 +56,7 @@ public void OnError(Exception exception) {
}
public void OnNext(IdeState snapshot) {
- logger.LogDebug($"IdeStateObserver.OnNext entered, threadId: {Thread.CurrentThread.ManagedThreadId}");
+ logger.LogTrace($"IdeStateObserver.OnNext entered, threadId: {Thread.CurrentThread.ManagedThreadId}");
lock (lastPublishedStateLock) {
if (snapshot.Version < LastPublishedState.Version) {
return;
diff --git a/Source/DafnyLanguageServer/Workspace/TextDocumentLoader.cs b/Source/DafnyLanguageServer/Workspace/TextDocumentLoader.cs
index cf2f449398c..73edeb83bb3 100644
--- a/Source/DafnyLanguageServer/Workspace/TextDocumentLoader.cs
+++ b/Source/DafnyLanguageServer/Workspace/TextDocumentLoader.cs
@@ -23,7 +23,6 @@ public class TextDocumentLoader : ITextDocumentLoader {
private const int ResolverMaxStackSize = 0x10000000; // 256MB
private static readonly ThreadTaskScheduler ResolverScheduler = new(ResolverMaxStackSize);
- private readonly DafnyOptions options;
private readonly IDafnyParser parser;
private readonly ISymbolResolver symbolResolver;
private readonly ISymbolTableFactory symbolTableFactory;
@@ -33,7 +32,6 @@ public class TextDocumentLoader : ITextDocumentLoader {
protected readonly INotificationPublisher NotificationPublisher;
protected TextDocumentLoader(
- DafnyOptions options,
ILoggerFactory loggerFactory,
IDafnyParser parser,
ISymbolResolver symbolResolver,
@@ -41,7 +39,6 @@ protected TextDocumentLoader(
IGhostStateDiagnosticCollector ghostStateDiagnosticCollector,
ICompilationStatusNotificationPublisher statusPublisher,
INotificationPublisher notificationPublisher) {
- this.options = options;
this.parser = parser;
this.symbolResolver = symbolResolver;
this.symbolTableFactory = symbolTableFactory;
@@ -61,7 +58,7 @@ public static TextDocumentLoader Create(
ILoggerFactory loggerFactory,
INotificationPublisher notificationPublisher
) {
- return new TextDocumentLoader(options, loggerFactory, parser, symbolResolver, symbolTableFactory, ghostStateDiagnosticCollector, statusPublisher, notificationPublisher);
+ return new TextDocumentLoader(loggerFactory, parser, symbolResolver, symbolTableFactory, ghostStateDiagnosticCollector, statusPublisher, notificationPublisher);
}
public IdeState CreateUnloaded(DocumentTextBuffer textDocument, CancellationToken cancellationToken) {
@@ -75,16 +72,16 @@ public IdeState CreateUnloaded(DocumentTextBuffer textDocument, CancellationToke
);
}
- public async Task LoadAsync(DocumentTextBuffer textDocument,
+ public async Task LoadAsync(DafnyOptions options, DocumentTextBuffer textDocument,
CancellationToken cancellationToken) {
#pragma warning disable CS1998
return await await Task.Factory.StartNew(
- async () => LoadInternal(textDocument, cancellationToken), cancellationToken,
+ async () => LoadInternal(options, textDocument, cancellationToken), cancellationToken,
#pragma warning restore CS1998
TaskCreationOptions.None, ResolverScheduler);
}
- private DocumentAfterParsing LoadInternal(DocumentTextBuffer textDocument,
+ private DocumentAfterParsing LoadInternal(DafnyOptions options, DocumentTextBuffer textDocument,
CancellationToken cancellationToken) {
var errorReporter = new DiagnosticErrorReporter(options, textDocument.Text, textDocument.Uri);
statusPublisher.SendStatusNotification(textDocument, CompilationStatus.ResolutionStarted);
@@ -124,7 +121,7 @@ IReadOnlyList diagnostics
textDocument,
diagnostics,
SymbolTable.Empty(),
- SignatureAndCompletionTable.Empty(options, textDocument),
+ SignatureAndCompletionTable.Empty(DafnyOptions.Default, textDocument),
new Dictionary(),
Array.Empty(),
false,
diff --git a/Test/cli/projectFile/badTypes/badTypes.dfy b/Test/cli/projectFile/badTypes/badTypes.dfy
new file mode 100644
index 00000000000..3aff32ba633
--- /dev/null
+++ b/Test/cli/projectFile/badTypes/badTypes.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Foo() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/Test/cli/projectFile/badTypes/dfyconfig.toml b/Test/cli/projectFile/badTypes/dfyconfig.toml
new file mode 100644
index 00000000000..e5c8d4481ce
--- /dev/null
+++ b/Test/cli/projectFile/badTypes/dfyconfig.toml
@@ -0,0 +1,4 @@
+includes = ["**/*"]
+
+[options]
+warn-shadowing = 3
\ No newline at end of file
diff --git a/Test/cli/projectFile/broken/dfyconfig.toml b/Test/cli/projectFile/broken/dfyconfig.toml
new file mode 100644
index 00000000000..3fe6323945a
--- /dev/null
+++ b/Test/cli/projectFile/broken/dfyconfig.toml
@@ -0,0 +1,4 @@
+includdddes = ["src/**/*"]
+
+[options]
+warn-shadowing = true
\ No newline at end of file
diff --git a/Test/cli/projectFile/broken/invalidOption.toml b/Test/cli/projectFile/broken/invalidOption.toml
new file mode 100644
index 00000000000..6ffb7c3e8aa
--- /dev/null
+++ b/Test/cli/projectFile/broken/invalidOption.toml
@@ -0,0 +1,3 @@
+includes = ["../src/input.dfy"]
+[options]
+does-not-exist = true
diff --git a/Test/cli/projectFile/dfyconfig.toml b/Test/cli/projectFile/dfyconfig.toml
new file mode 100644
index 00000000000..48c886a1b0f
--- /dev/null
+++ b/Test/cli/projectFile/dfyconfig.toml
@@ -0,0 +1,5 @@
+includes = ["src/**/*"]
+excludes = ["**/excluded.dfy"]
+
+[options]
+warn-shadowing = true
\ No newline at end of file
diff --git a/Test/cli/projectFile/noIncludes/dfyconfig.toml b/Test/cli/projectFile/noIncludes/dfyconfig.toml
new file mode 100644
index 00000000000..39b81f42b08
--- /dev/null
+++ b/Test/cli/projectFile/noIncludes/dfyconfig.toml
@@ -0,0 +1,2 @@
+[options]
+warn-shadowing = true
\ No newline at end of file
diff --git a/Test/cli/projectFile/noIncludes/src/input.dfy b/Test/cli/projectFile/noIncludes/src/input.dfy
new file mode 100644
index 00000000000..3aff32ba633
--- /dev/null
+++ b/Test/cli/projectFile/noIncludes/src/input.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Foo() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/Test/cli/projectFile/noIncludes/src/moreNested/moreInput.dfy b/Test/cli/projectFile/noIncludes/src/moreNested/moreInput.dfy
new file mode 100644
index 00000000000..d05a03de449
--- /dev/null
+++ b/Test/cli/projectFile/noIncludes/src/moreNested/moreInput.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Bar() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/Test/cli/projectFile/notInput.dfy b/Test/cli/projectFile/notInput.dfy
new file mode 100644
index 00000000000..0e5bca9d1d3
--- /dev/null
+++ b/Test/cli/projectFile/notInput.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Foo() {
+ var y := 3;
+ if (true) {
+ var y := 4;
+ }
+}
\ No newline at end of file
diff --git a/Test/cli/projectFile/projectFile.dfy b/Test/cli/projectFile/projectFile.dfy
new file mode 100644
index 00000000000..8f57479d67c
--- /dev/null
+++ b/Test/cli/projectFile/projectFile.dfy
@@ -0,0 +1,34 @@
+// A project file can specify input files and configure options
+// RUN: %baredafny resolve --use-basename-for-filename "%S/dfyconfig.toml" > "%t"
+
+// Test using a URL instead of a local file as a project file
+// RUN: ! %baredafny resolve --use-basename-for-filename "https://github.com/dafny-lang/dafny/blob/master/dfyconfig.toml"
+
+// Test option override behavior
+// RUN: %baredafny resolve --use-basename-for-filename "%S/dfyconfig.toml" --warn-shadowing=false >> "%t"
+
+// Multiple project files are not allowed
+// RUN: ! %baredafny resolve --use-basename-for-filename "%S/dfyconfig.toml" "%S/broken/dfyconfig.toml"
+
+// Project files may not contain unknown properties
+// RUN: ! %baredafny resolve --use-basename-for-filename "%S/broken/dfyconfig.toml"
+
+// Warn if file contains options that don't exist
+// RUN: %baredafny resolve --use-basename-for-filename "%S/broken/invalidOption.toml" >> "%t"
+
+// Project files must be files on disk.
+// RUN: ! %baredafny resolve --use-basename-for-filename "%S/doesNotExist/dfyconfig.toml"
+
+// Project file options must have the right type
+// RUN: ! %baredafny resolve --use-basename-for-filename "%S/badTypes/dfyconfig.toml" 2>> "%t"
+
+// A project file without includes will take all .dfy files as input
+// RUN: %baredafny resolve --use-basename-for-filename "%S/noIncludes/dfyconfig.toml" >> "%t"
+
+// Files included by the project file and on the CLI, duplicate is ignored.
+// RUN: %baredafny resolve --use-basename-for-filename "%S/dfyconfig.toml" "%S/src/input.dfy" >> "%t"
+
+// Files excluded by the project file and included on the CLI, are included
+// RUN: ! %baredafny resolve --use-basename-for-filename "%S/dfyconfig.toml" "%S/src/excluded.dfy" >> "%t"
+
+// RUN: %diff "%s.expect" "%t"
\ No newline at end of file
diff --git a/Test/cli/projectFile/projectFile.dfy.expect b/Test/cli/projectFile/projectFile.dfy.expect
new file mode 100644
index 00000000000..55f569deeeb
--- /dev/null
+++ b/Test/cli/projectFile/projectFile.dfy.expect
@@ -0,0 +1,21 @@
+input.dfy(6,8): Warning: Shadowed local-variable name: x
+
+Dafny program verifier did not attempt verification
+
+Dafny program verifier did not attempt verification
+Warning: only Dafny project files named dfyconfig.toml are recognised by the Dafny IDE.
+Warning: option 'does-not-exist' that was specified in the project file, is not a valid Dafny option.
+
+Dafny program verifier did not attempt verification
+Error: property 'warn-shadowing' is of type 'Int64' but should be of type 'Boolean'
+input.dfy(6,8): Warning: Shadowed local-variable name: x
+moreInput.dfy(6,8): Warning: Shadowed local-variable name: x
+
+Dafny program verifier did not attempt verification
+input.dfy(6,8): Warning: Shadowed local-variable name: x
+
+Dafny program verifier did not attempt verification
+excluded.dfy(3,7): Error: Duplicate member name: Foo
+input.dfy(6,8): Warning: Shadowed local-variable name: x
+excluded.dfy(6,8): Warning: Shadowed local-variable name: z
+1 resolution/type errors detected in the_program
diff --git a/Test/cli/projectFile/src/excluded.dfy b/Test/cli/projectFile/src/excluded.dfy
new file mode 100644
index 00000000000..ec506c35223
--- /dev/null
+++ b/Test/cli/projectFile/src/excluded.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Foo() {
+ var z := 3;
+ if (true) {
+ var z := 4;
+ }
+}
\ No newline at end of file
diff --git a/Test/cli/projectFile/src/input.dfy b/Test/cli/projectFile/src/input.dfy
new file mode 100644
index 00000000000..3aff32ba633
--- /dev/null
+++ b/Test/cli/projectFile/src/input.dfy
@@ -0,0 +1,8 @@
+// RUN: echo ""
+
+method Foo() {
+ var x := 3;
+ if (true) {
+ var x := 4;
+ }
+}
\ No newline at end of file
diff --git a/customBoogie.patch b/customBoogie.patch
index a71499bcc71..5bfb4c1f360 100644
--- a/customBoogie.patch
+++ b/customBoogie.patch
@@ -54,15 +54,15 @@ index 426b132e2..18db4aebb 100644
EndGlobalSection
EndGlobal
diff --git a/Source/DafnyCore/DafnyCore.csproj b/Source/DafnyCore/DafnyCore.csproj
-index 0f17aa923..6b937ebcb 100644
+index 4a8b2f89b..a308be9bf 100644
--- a/Source/DafnyCore/DafnyCore.csproj
+++ b/Source/DafnyCore/DafnyCore.csproj
-@@ -30,7 +30,7 @@
+@@ -31,7 +31,7 @@
-
+
+
-
diff --git a/docs/DafnyRef/UserGuide.md b/docs/DafnyRef/UserGuide.md
index 0b3e446da00..7d9dbb0f892 100644
--- a/docs/DafnyRef/UserGuide.md
+++ b/docs/DafnyRef/UserGuide.md
@@ -571,6 +571,25 @@ Most output from `dafny` is directed to the standard output of the shell invokin
- Dafny `expect` statements (when they fail) send a message to **standard-out**.
- Dafny I/O libraries send output explicitly to either **standard-out or standard-error**
+### 13.5.5. Project files
+
+Commands on the Dafny CLI that can be passed a Dafny file, can also be passed a Dafny project file. Such a project file may define which Dafny files the project contains, and which Dafny options it uses. The project file must be a [TOML](https://toml.io/en/) file named `dfyconfig.toml` for it to work on both the CLI and in the Dafny IDE, although the CLI will accept any `.toml` file. Here's an example of a Dafny project file:
+
+```toml
+includes = ["src/**/*.dfy"]
+excludes = ["**/ignore.dfy"]
+
+[options]
+enforce-determinism = true
+warn-shadowing = true
+```
+
+Under the section `[options]`, any options from the Dafny CLI can be specified using the option's name without the `--` prefix. When executing a `dafny` command using a project file, any options specified in the file that can be applied to the command, will be. Options that can't be applied or are misspelled, are ignored.
+
+When using a Dafny IDE based on the `dafny server` command, the IDE will search for project files by traversing up the file tree looking for the closest `dfyconfig.toml` file it can find. Options from the project file will override options passed to `dafny server`.
+
+It's not possible to use Dafny project files in combination with the legacy CLI UI.
+
## 13.6. Verification {#sec-verification}
In this section, we suggest a methodology to figure out [why a single assertion might not hold](#sec-verification-debugging), we propose techniques to deal with [assertions that slow a proof down](#sec-verification-debugging-slow), we explain how to [verify assertions in parallel or in a focused way](#sec-assertion-batches), and we also give some more examples of [useful options and attributes to control verification](#sec-command-line-options-and-attributes-for-verification).
diff --git a/docs/dev/news/2907.feat b/docs/dev/news/2907.feat
new file mode 100644
index 00000000000..50694c4db32
--- /dev/null
+++ b/docs/dev/news/2907.feat
@@ -0,0 +1,3 @@
+Added support for `.toml` based Dafny project files. For now the project file only allows specifying which Dafny files to include and exclude, and what options to use.
+The CLI commands that take Dafny files as input, such as build, run, translate, will now also accept Dafny project files.
+When using an IDE based on `dafny server`, such as the Dafny VSCode extension, the IDE will look for a Dafny project file by traversing up the file tree from the currently opened file, until it finds it `dfyconfig.toml`. The project file will override options specified in the IDE.
\ No newline at end of file