Skip to content

Commit d795504

Browse files
authored
Add --disableLanguageFeature CLI switch and MSBuild property to selectively disable language features (#19167)
1 parent a9d3ead commit d795504

36 files changed

+394
-32
lines changed

.github/copilot-instructions.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ Always run the core command. Always verify exit codes. No assumptions.
6262
```
6363
Non‑zero → classify & stop.
6464

65+
### CRITICAL TEST EXECUTION RULES
66+
**ALWAYS** run tests before claiming success. **NEVER** mark work complete without verified passing tests.
67+
68+
When running tests, **ALWAYS** report:
69+
- Total number of tests executed
70+
- Number passed / failed / skipped
71+
- Execution duration
72+
- Example: "Ran 5 tests: 5 passed, 0 failed, 0 skipped. Duration: 4.2 seconds"
73+
74+
**ASSUME YOUR CODE IS THE PROBLEM**: When tests fail, ALWAYS assume your implementation is incorrect FIRST. Only after thorough investigation with evidence should you consider other causes like build issues or test infrastructure problems.
75+
76+
**UNDERSTAND WHAT YOU'RE TESTING**: Before writing tests, understand exactly what behavior the feature controls. Research the codebase to see how the feature is actually used, not just how you think it should work.
77+
78+
**TEST INCREMENTALLY**: After each code change, immediately run the relevant tests to verify the change works as expected. Don't accumulate multiple changes before testing.
79+
6580
## 2. Bootstrap (Failure Detection Only)
6681
Two-phase build. No separate bootstrap command.
6782
Early proto/tool errors (e.g. "Error building tools") → `BootstrapFailure` (capture key lines). Stop.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
* Add `--disableLanguageFeature` CLI switch and MSBuild property to selectively disable specific F# language features on a per-project basis. ([PR #19167](https://github.com/dotnet/fsharp/pull/19167))

src/Compiler/Driver/CompilerConfig.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,8 @@ type TcConfigBuilder =
637637

638638
mutable langVersion: LanguageVersion
639639

640+
mutable disabledLanguageFeatures: Set<LanguageFeature>
641+
640642
mutable xmlDocInfoLoader: IXmlDocumentationInfoLoader option
641643

642644
mutable exiter: Exiter
@@ -827,6 +829,7 @@ type TcConfigBuilder =
827829
pathMap = PathMap.empty
828830
applyLineDirectives = true
829831
langVersion = LanguageVersion.Default
832+
disabledLanguageFeatures = Set.empty
830833
implicitIncludeDir = implicitIncludeDir
831834
defaultFSharpBinariesDir = defaultFSharpBinariesDir
832835
reduceMemoryUsage = reduceMemoryUsage
@@ -1402,6 +1405,7 @@ type TcConfig private (data: TcConfigBuilder, validate: bool) =
14021405
member _.CloneToBuilder() =
14031406
{ data with
14041407
conditionalDefines = data.conditionalDefines
1408+
disabledLanguageFeatures = data.disabledLanguageFeatures
14051409
}
14061410

14071411
member tcConfig.ComputeCanContainEntryPoint(sourceFiles: string list) =

src/Compiler/Driver/CompilerConfig.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ type TcConfigBuilder =
507507

508508
mutable langVersion: LanguageVersion
509509

510+
mutable disabledLanguageFeatures: Set<LanguageFeature>
511+
510512
mutable xmlDocInfoLoader: IXmlDocumentationInfoLoader option
511513

512514
mutable exiter: Exiter

src/Compiler/Driver/CompilerOptions.fs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,11 +1168,28 @@ let languageFlags tcConfigB =
11681168
CompilerOption(
11691169
"langversion",
11701170
tagLangVersionValues,
1171-
OptionString(fun switch -> tcConfigB.langVersion <- setLanguageVersion switch),
1171+
OptionString(fun switch ->
1172+
let newVersion = setLanguageVersion switch
1173+
// Preserve disabled features when updating version
1174+
tcConfigB.langVersion <- newVersion.WithDisabledFeatures(Set.toArray tcConfigB.disabledLanguageFeatures)),
11721175
None,
11731176
Some(FSComp.SR.optsSetLangVersion ())
11741177
)
11751178

1179+
// -disableLanguageFeature:<string> Disable a specific language feature by name (repeatable)
1180+
CompilerOption(
1181+
"disableLanguageFeature",
1182+
tagString,
1183+
OptionStringList(fun featureName ->
1184+
match LanguageVersion.TryParseFeature(featureName) with
1185+
| Some feature ->
1186+
tcConfigB.disabledLanguageFeatures <- Set.add feature tcConfigB.disabledLanguageFeatures
1187+
tcConfigB.langVersion <- tcConfigB.langVersion.WithDisabledFeatures(Set.toArray tcConfigB.disabledLanguageFeatures)
1188+
| None -> error (Error(FSComp.SR.optsUnrecognizedLanguageFeature featureName, rangeCmdArgs))),
1189+
None,
1190+
Some(FSComp.SR.optsDisableLanguageFeature ())
1191+
)
1192+
11761193
CompilerOption(
11771194
"checked",
11781195
tagNone,

src/Compiler/FSComp.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,7 @@ optsCheckNulls,"Enable nullness declarations and checks (%s by default)"
15551555
fSharpBannerVersion,"%s for F# %s"
15561556
optsGetLangVersions,"Display the allowed values for language version."
15571557
optsSetLangVersion,"Specify language version such as 'latest' or 'preview'."
1558+
optsDisableLanguageFeature,"Disable a specific language feature by name."
15581559
optsSupportedLangVersions,"Supported language versions:"
15591560
optsStrictIndentation,"Override indentation rules implied by the language version (%s by default)"
15601561
nativeResourceFormatError,"Stream does not begin with a null resource and is not in '.RES' format."
@@ -1801,4 +1802,5 @@ featureAllowLetOrUseBangTypeAnnotationWithoutParens,"Allow let! and use! type an
18011802
3878,tcAttributeIsNotValidForUnionCaseWithFields,"This attribute is not valid for use on union cases with fields."
18021803
3879,xmlDocNotFirstOnLine,"XML documentation comments should be the first non-whitespace text on a line."
18031804
featureReturnFromFinal,"Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder."
1804-
3879,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s"
1805+
3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s"
1806+
3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'."

src/Compiler/Facilities/LanguageFeatures.fs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ type LanguageFeature =
106106
| ReturnFromFinal
107107

108108
/// LanguageVersion management
109-
type LanguageVersion(versionText) =
109+
type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) =
110110

111111
// When we increment language versions here preview is higher than current RTM version
112112
static let languageVersion46 = 4.6m
@@ -287,11 +287,22 @@ type LanguageVersion(versionText) =
287287

288288
let specifiedString = versionToString specified
289289

290+
let disabledFeatures: LanguageFeature array = defaultArg disabledFeaturesArray [||]
291+
292+
/// Get the disabled features
293+
member _.DisabledFeatures = disabledFeatures
294+
290295
/// Check if this feature is supported by the selected langversion
291296
member _.SupportsFeature featureId =
292-
match features.TryGetValue featureId with
293-
| true, v -> v <= specified
294-
| false, _ -> false
297+
if Array.contains featureId disabledFeatures then
298+
false
299+
else
300+
match features.TryGetValue featureId with
301+
| true, v -> v <= specified
302+
| false, _ -> false
303+
304+
/// Create a new LanguageVersion with updated disabled features
305+
member _.WithDisabledFeatures(disabled: LanguageFeature array) = LanguageVersion(versionText, disabled)
295306

296307
/// Has preview been explicitly specified
297308
member _.IsExplicitlySpecifiedAs50OrBefore() =
@@ -436,11 +447,38 @@ type LanguageVersion(versionText) =
436447
| true, v -> versionToString v
437448
| _ -> invalidArg "feature" "Internal error: Unable to find feature."
438449

450+
/// Try to parse a feature name string to a LanguageFeature option using reflection
451+
static member TryParseFeature(featureName: string) =
452+
let normalized = featureName.Trim()
453+
454+
let bindingFlags =
455+
System.Reflection.BindingFlags.Public
456+
||| System.Reflection.BindingFlags.NonPublic
457+
458+
Microsoft.FSharp.Reflection.FSharpType.GetUnionCases(typeof<LanguageFeature>, bindingFlags)
459+
|> Array.tryFind (fun case -> System.String.Equals(case.Name, normalized, System.StringComparison.OrdinalIgnoreCase))
460+
|> Option.bind (fun case ->
461+
let union =
462+
Microsoft.FSharp.Reflection.FSharpValue.MakeUnion(case, [||], bindingFlags)
463+
464+
match box union with
465+
| null -> None
466+
| obj -> Some(obj :?> LanguageFeature))
467+
439468
override x.Equals(yobj: obj) =
440469
match yobj with
441-
| :? LanguageVersion as y -> x.SpecifiedVersion = y.SpecifiedVersion
470+
| :? LanguageVersion as y ->
471+
x.SpecifiedVersion = y.SpecifiedVersion
472+
&& x.DisabledFeatures.Length = y.DisabledFeatures.Length
473+
&& (x.DisabledFeatures, y.DisabledFeatures) ||> Array.forall2 (=)
442474
| _ -> false
443475

444-
override x.GetHashCode() = hash x.SpecifiedVersion
476+
override x.GetHashCode() =
477+
let mutable h = hash x.SpecifiedVersion
478+
479+
for f in x.DisabledFeatures do
480+
h <- h ^^^ hash f
481+
482+
h
445483

446484
static member Default = defaultLanguageVersion

src/Compiler/Facilities/LanguageFeatures.fsi

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ type LanguageFeature =
100100
type LanguageVersion =
101101

102102
/// Create a LanguageVersion management object
103-
new: string -> LanguageVersion
103+
new: string * ?disabledFeaturesArray: LanguageFeature array -> LanguageVersion
104104

105105
/// Is the selected LanguageVersion valid
106106
static member ContainsVersion: string -> bool
@@ -117,6 +117,12 @@ type LanguageVersion =
117117
/// Does the selected LanguageVersion support the specified feature
118118
member SupportsFeature: LanguageFeature -> bool
119119

120+
/// Get the disabled features
121+
member DisabledFeatures: LanguageFeature array
122+
123+
/// Create a new LanguageVersion with updated disabled features
124+
member WithDisabledFeatures: LanguageFeature array -> LanguageVersion
125+
120126
/// Get the list of valid versions
121127
static member ValidVersions: string[]
122128

@@ -138,4 +144,7 @@ type LanguageVersion =
138144
/// Get a version string associated with the given feature.
139145
static member GetFeatureVersionString: feature: LanguageFeature -> string
140146

147+
/// Try to parse a feature name string to a LanguageFeature option
148+
static member TryParseFeature: featureName: string -> LanguageFeature option
149+
141150
static member Default: LanguageVersion

src/Compiler/Service/IncrementalBuild.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ type FrameworkImportsCache(size) =
556556
// for each cached project. So here we create a new tcGlobals, with the existing framework values
557557
// and updated realsig and langversion
558558
let tcGlobals =
559-
if tcGlobals.langVersion.SpecifiedVersion <> tcConfig.langVersion.SpecifiedVersion
559+
if tcGlobals.langVersion <> tcConfig.langVersion
560560
|| tcGlobals.realsig <> tcConfig.realsig then
561561
TcGlobals(
562562
tcGlobals.compilingFSharpCore,

src/Compiler/Service/TransparentCompiler.fs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,12 @@ type internal TransparentCompiler
490490

491491
let applyCompilerOptions tcConfig =
492492
let fsiCompilerOptions = GetCoreFsiCompilerOptions tcConfig
493-
ParseCompilerOptions(ignore, fsiCompilerOptions, otherOptions)
493+
494+
try
495+
ParseCompilerOptions(ignore, fsiCompilerOptions, otherOptions)
496+
with
497+
| :? OperationCanceledException -> reraise ()
498+
| exn -> errorRecovery exn range0
494499

495500
let closure =
496501
LoadClosure.ComputeClosureOfScriptText(
@@ -900,8 +905,16 @@ type internal TransparentCompiler
900905
tcConfigB.useSimpleResolution <- useSimpleResolution
901906

902907
// Apply command-line arguments and collect more source files if they are in the arguments
908+
// Wrap in try/catch to ensure command-line parsing errors are properly captured
909+
// as diagnostics rather than escaping as exceptions
903910
let sourceFilesNew =
904-
ApplyCommandLineArgs(tcConfigB, projectSnapshot.SourceFileNames, commandLineArgs)
911+
try
912+
ApplyCommandLineArgs(tcConfigB, projectSnapshot.SourceFileNames, commandLineArgs)
913+
with
914+
| :? OperationCanceledException -> reraise ()
915+
| exn ->
916+
errorRecovery exn range0
917+
projectSnapshot.SourceFileNames
905918

906919
// Never open PDB files for the language service, even if --standalone is specified
907920
tcConfigB.openDebugInformationForLaterStaticLinking <- false
@@ -949,6 +962,33 @@ type internal TransparentCompiler
949962
// Prepare the frameworkTcImportsCache
950963
let! tcGlobals, frameworkTcImports = ComputeFrameworkImports tcConfig frameworkDLLs nonFrameworkResolutions
951964

965+
// If the tcGlobals was loaded from a different project, langVersion and realsig may be different
966+
// for each cached project. So here we create a new tcGlobals, with the existing framework values
967+
// and updated realsig and langversion
968+
let tcGlobals =
969+
if
970+
tcGlobals.langVersion <> tcConfig.langVersion
971+
|| tcGlobals.realsig <> tcConfig.realsig
972+
then
973+
TcGlobals(
974+
tcGlobals.compilingFSharpCore,
975+
tcGlobals.ilg,
976+
tcGlobals.fslibCcu,
977+
tcGlobals.directoryToResolveRelativePaths,
978+
tcGlobals.isInteractive,
979+
tcGlobals.checkNullness,
980+
tcGlobals.useReflectionFreeCodeGen,
981+
tcGlobals.tryFindSysTypeCcuHelper,
982+
tcGlobals.emitDebugInfoInQuotations,
983+
tcGlobals.noDebugAttributes,
984+
tcGlobals.pathMap,
985+
tcConfig.langVersion,
986+
tcConfig.realsig,
987+
tcConfig.compilationMode
988+
)
989+
else
990+
tcGlobals
991+
952992
// Note we are not calling diagnosticsLogger.GetDiagnostics() anywhere for this task.
953993
// This is ok because not much can actually go wrong here.
954994
let diagnosticsLogger =
@@ -1013,7 +1053,6 @@ type internal TransparentCompiler
10131053

10141054
let computeBootstrapInfoInner (projectSnapshot: ProjectSnapshot) =
10151055
async {
1016-
10171056
let! tcConfigB, sourceFiles, loadClosureOpt = ComputeTcConfigBuilder projectSnapshot
10181057

10191058
// If this is a builder for a script, re-apply the settings inferred from the

0 commit comments

Comments
 (0)