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 support for C# Experimental attribute #18253

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/9.0.300.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Added
* Added missing type constraints in FCS. ([PR #18241](https://github.com/dotnet/fsharp/pull/18241))
* Add support for C# `Experimental` attribute. ([PR #18253](https://github.com/dotnet/fsharp/pull/18253))

### Changed

Expand Down
118 changes: 67 additions & 51 deletions src/Compiler/Checking/AttributeChecking.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ open FSharp.Compiler.TypeProviders
open FSharp.Core.CompilerServices
#endif

exception ObsoleteDiagnostic of
isError: bool *
diagnosticId: string *
message: string *
urlFormat: string *
range: range

let fail() = failwith "This custom attribute has an argument that cannot yet be converted using this API"

let rec private evalILAttribElem elem =
Expand Down Expand Up @@ -247,44 +240,68 @@ let private CheckCompilerFeatureRequiredAttribute (g: TcGlobals) cattrs msg m =
| Some([ILAttribElem.String (Some featureName) ], _) when featureName = "RequiredMembers" ->
CompleteD
| _ ->
ErrorD (ObsoleteDiagnostic(true, "", msg, "", m))
ErrorD (ObsoleteDiagnostic(true, None, msg, None, m))

let private extractILAttribValueFrom name namedArgs =
match namedArgs with
| ExtractILAttributeNamedArg name (AttribElemStringArg v) -> Some v
| _ -> None

let private extractILObsoleteAttributeInfo namedArgs =
let extractILAttribValueFrom name namedArgs =
match namedArgs with
| ExtractILAttributeNamedArg name (AttribElemStringArg v) -> v
| _ -> ""
let private extractILAttributeInfo namedArgs =
let diagnosticId = extractILAttribValueFrom "DiagnosticId" namedArgs
let urlFormat = extractILAttribValueFrom "UrlFormat" namedArgs
(diagnosticId, urlFormat)

let private CheckILExperimentalAttributes (g: TcGlobals) cattrs m =
let (AttribInfo(tref,_)) = g.attrib_IlExperimentalAttribute
match TryDecodeILAttribute tref cattrs with
// [Experimental("DiagnosticId")]
// [Experimental(diagnosticId: "DiagnosticId")]
// [Experimental("DiagnosticId", UrlFormat = "UrlFormat")]
// [Experimental(diagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")]
// Constructors deciding on DiagnosticId and UrlFormat properties.
| Some ([ attribElement ], namedArgs) ->
let diagnosticId =
match attribElement with
| ILAttribElem.String (Some msg) -> Some msg
| ILAttribElem.String None
| _ -> None

let message = extractILAttribValueFrom "Message" namedArgs
let urlFormat = extractILAttribValueFrom "UrlFormat" namedArgs

WarnD(Experimental(message, diagnosticId, urlFormat, m))
// Empty constructor or only UrlFormat property are not allowed.
| Some _
| None -> CompleteD

let private CheckILObsoleteAttributes (g: TcGlobals) isByrefLikeTyconRef cattrs m =
if isByrefLikeTyconRef then
CompleteD
else
let (AttribInfo(tref,_)) = g.attrib_SystemObsolete
match TryDecodeILAttribute tref cattrs with
// [<Obsolete>]
// [<Obsolete("Message")>]
// [<Obsolete("Message", true)>]
// [<Obsolete("Message", DiagnosticId = "DiagnosticId")>]
// [<Obsolete("Message", DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")>]
// [<Obsolete(DiagnosticId = "DiagnosticId")>]
// [<Obsolete(DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")>]
// [<Obsolete("Message", true, DiagnosticId = "DiagnosticId")>]
// [<Obsolete("Message", true, DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")>]
// [Obsolete>]
// [Obsolete("Message")]
// [Obsolete("Message", true)]
// [Obsolete("Message", DiagnosticId = "DiagnosticId")]
// [Obsolete("Message", DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")]
// [Obsolete(DiagnosticId = "DiagnosticId")]
// [Obsolete(DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")]
// [Obsolete("Message", true, DiagnosticId = "DiagnosticId")]
// [Obsolete("Message", true, DiagnosticId = "DiagnosticId", UrlFormat = "UrlFormat")]
// Constructors deciding on IsError and Message properties.
| Some ([ attribElement ], namedArgs) ->
let diagnosticId, urlFormat = extractILObsoleteAttributeInfo namedArgs
let diagnosticId, urlFormat = extractILAttributeInfo namedArgs
let msg =
match attribElement with
| ILAttribElem.String (Some msg) -> msg
| ILAttribElem.String (Some msg) -> Some msg
| ILAttribElem.String None
| _ -> ""
| _ -> None

WarnD (ObsoleteDiagnostic(false, diagnosticId, msg, urlFormat, m))
| Some ([ILAttribElem.String (Some msg); ILAttribElem.Bool isError ], namedArgs) ->
let diagnosticId, urlFormat = extractILObsoleteAttributeInfo namedArgs
| Some ([ILAttribElem.String msg; ILAttribElem.Bool isError ], namedArgs) ->
let diagnosticId, urlFormat = extractILAttributeInfo namedArgs
if isError then
if g.langVersion.SupportsFeature(LanguageFeature.RequiredPropertiesSupport) then
CheckCompilerFeatureRequiredAttribute g cattrs msg m
Expand All @@ -294,24 +311,23 @@ let private CheckILObsoleteAttributes (g: TcGlobals) isByrefLikeTyconRef cattrs
WarnD (ObsoleteDiagnostic(false, diagnosticId, msg, urlFormat, m))
// Only DiagnosticId, UrlFormat
| Some (_, namedArgs) ->
let diagnosticId, urlFormat = extractILObsoleteAttributeInfo namedArgs
WarnD(ObsoleteDiagnostic(false, diagnosticId, "", urlFormat, m))
let diagnosticId, urlFormat = extractILAttributeInfo namedArgs
WarnD(ObsoleteDiagnostic(false, diagnosticId, None, urlFormat, m))
// No arguments
| None -> CompleteD

/// Check IL attributes for 'ObsoleteAttribute', returning errors and warnings as data
/// Check IL attributes for Experimental, warnings as data
let private CheckILAttributes (g: TcGlobals) isByrefLikeTyconRef cattrs m =
trackErrors {
do! CheckILObsoleteAttributes g isByrefLikeTyconRef cattrs m
do! CheckILExperimentalAttributes g cattrs m
}

let langVersionPrefix = "--langversion:preview"

let private extractObsoleteAttributeInfo namedArgs =
let extractILAttribValueFrom name namedArgs =
match namedArgs with
| ExtractAttribNamedArg name (AttribStringArg v) -> v
| _ -> ""
| ExtractAttribNamedArg name (AttribStringArg v) -> Some v
| _ -> None
let diagnosticId = extractILAttribValueFrom "DiagnosticId" namedArgs
let urlFormat = extractILAttribValueFrom "UrlFormat" namedArgs
(diagnosticId, urlFormat)
Expand All @@ -331,17 +347,17 @@ let private CheckObsoleteAttributes g attribs m =
// Constructors deciding on IsError and Message properties.
| Some(Attrib(unnamedArgs= [ AttribStringArg s ]; propVal= namedArgs)) ->
let diagnosticId, urlFormat = extractObsoleteAttributeInfo namedArgs
do! WarnD(ObsoleteDiagnostic(false, diagnosticId, s, urlFormat, m))
do! WarnD(ObsoleteDiagnostic(false, diagnosticId, Some s, urlFormat, m))
| Some(Attrib(unnamedArgs= [ AttribStringArg s; AttribBoolArg(isError) ]; propVal= namedArgs)) ->
let diagnosticId, urlFormat = extractObsoleteAttributeInfo namedArgs
if isError then
do! ErrorD (ObsoleteDiagnostic(true, diagnosticId, s, urlFormat, m))
do! ErrorD (ObsoleteDiagnostic(true, diagnosticId, Some s, urlFormat, m))
else
do! WarnD (ObsoleteDiagnostic(false, diagnosticId, s, urlFormat, m))
do! WarnD (ObsoleteDiagnostic(false, diagnosticId, Some s, urlFormat, m))
// Only DiagnosticId, UrlFormat
| Some(Attrib(propVal= namedArgs)) ->
let diagnosticId, urlFormat = extractObsoleteAttributeInfo namedArgs
do! WarnD(ObsoleteDiagnostic(false, diagnosticId, "", urlFormat, m))
do! WarnD(ObsoleteDiagnostic(false, diagnosticId, None, urlFormat, m))
| None -> ()
}

Expand All @@ -366,21 +382,21 @@ let private CheckCompilerMessageAttribute g attribs m =
()
}

let private CheckExperimentalAttribute g attribs m =
let private CheckFSharpExperimentalAttribute g attribs m =
trackErrors {
match TryFindFSharpAttribute g g.attrib_ExperimentalAttribute attribs with
// [<Experimental("Message")>]
| Some(Attrib(unnamedArgs= [ AttribStringArg(s) ])) ->
let isExperimentalAttributeDisabled (s:string) =
if g.compilingFSharpCore then
true
else
g.langVersion.IsPreviewEnabled && (s.IndexOf(langVersionPrefix, StringComparison.OrdinalIgnoreCase) >= 0)
g.langVersion.IsPreviewEnabled && (s.IndexOf("--langversion:preview", StringComparison.OrdinalIgnoreCase) >= 0)
if not (isExperimentalAttributeDisabled s) then
do! WarnD(Experimental(s, m))
| Some _ ->
do! WarnD(Experimental(FSComp.SR.experimentalConstruct (), m))
| _ ->
()
do! WarnD(Experimental(Some s, None, None, m))
// Empty constructor is not allowed.
| Some _
| _ -> ()
}

let private CheckUnverifiableAttribute g attribs m =
Expand All @@ -399,7 +415,7 @@ let CheckFSharpAttributes (g:TcGlobals) attribs m =
trackErrors {
do! CheckObsoleteAttributes g attribs m
do! CheckCompilerMessageAttribute g attribs m
do! CheckExperimentalAttribute g attribs m
do! CheckFSharpExperimentalAttribute g attribs m
do! CheckUnverifiableAttribute g attribs m
}

Expand All @@ -408,16 +424,16 @@ let CheckFSharpAttributes (g:TcGlobals) attribs m =
let private CheckProvidedAttributes (g: TcGlobals) m (provAttribs: Tainted<IProvidedCustomAttributeProvider>) =
let (AttribInfo(tref, _)) = g.attrib_SystemObsolete
match provAttribs.PUntaint((fun a -> a.GetAttributeConstructorArgs(provAttribs.TypeProvider.PUntaintNoFailure(id), tref.FullName)), m) with
| Some ([ Some (:? string as msg) ], _) -> WarnD(ObsoleteDiagnostic(false, "", msg, "", m))
| Some ([ Some (:? string as msg) ], _) -> WarnD(ObsoleteDiagnostic(false, None, Some msg, None, m))
| Some ([ Some (:? string as msg); Some (:?bool as isError) ], _) ->
if isError then
ErrorD (ObsoleteDiagnostic(true, "", msg, "", m))
ErrorD (ObsoleteDiagnostic(true, None, Some msg, None, m))
else
WarnD (ObsoleteDiagnostic(false, "", msg, "", m))
WarnD (ObsoleteDiagnostic(false, None, Some msg, None, m))
| Some ([ None ], _) ->
WarnD(ObsoleteDiagnostic(false, "", "", "", m))
WarnD(ObsoleteDiagnostic(false, None, None, None, m))
| Some _ ->
WarnD(ObsoleteDiagnostic(false, "", "", "", m))
WarnD(ObsoleteDiagnostic(false, None, None, None, m))
| None ->
CompleteD
#endif
Expand Down
7 changes: 0 additions & 7 deletions src/Compiler/Checking/AttributeChecking.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ open FSharp.Compiler.TcGlobals
open FSharp.Compiler.Text
open FSharp.Compiler.TypedTree

exception ObsoleteDiagnostic of
isError: bool *
diagnosticId: string *
message: string *
urlFormat: string *
range: range

type AttribInfo =
| FSAttribInfo of TcGlobals * Attrib
| ILAttribInfo of TcGlobals * Import.ImportMap * ILScopeRef * ILAttribute * range
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Checking/PostInferenceChecks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ let WarnOnWrongTypeForAccess (cenv: cenv) env objName valAcc m ty =
if isLessAccessible tyconAcc valAcc then
let errorText = FSComp.SR.chkTypeLessAccessibleThanType(tcref.DisplayName, (objName())) |> snd
let warningText = errorText + Environment.NewLine + FSComp.SR.tcTypeAbbreviationsCheckedAtCompileTime()
warning(AttributeChecking.ObsoleteDiagnostic(false, "", warningText, "", m))
warning(ObsoleteDiagnostic(false, None, Some warningText, None, m))

CheckTypeDeep cenv (visitType, None, None, None, None) cenv.g env NoInfo ty

Expand Down
23 changes: 16 additions & 7 deletions src/Compiler/Driver/CompilerDiagnostics.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ open Internal.Utilities.Library
open Internal.Utilities.Text

open FSharp.Compiler
open FSharp.Compiler.AttributeChecking
open FSharp.Compiler.CheckExpressions
open FSharp.Compiler.CheckDeclarations
open FSharp.Compiler.CheckIncrementalClasses
Expand Down Expand Up @@ -149,7 +148,7 @@ type Exception with
| ValueRestriction(_, _, _, _, m)
| LetRecUnsound(_, _, m)
| ObsoleteDiagnostic(_, _, _, _, m)
| Experimental(_, m)
| Experimental(range = m)
| PossibleUnverifiableCode m
| UserCompilerMessage(_, _, m)
| Deprecated(_, m)
Expand Down Expand Up @@ -566,7 +565,9 @@ module OldStyleMessages =
let ValNotLocalE () = Message("ValNotLocal", "")
let Obsolete1E () = Message("Obsolete1", "")
let Obsolete2E () = Message("Obsolete2", "%s")
let ExperimentalE () = Message("Experimental", "%s")
let Experimental1E () = Message("Experimental1", "")
let Experimental2E () = Message("Experimental2", "%s")
let Experimental3E () = Message("Experimental3", "")
let PossibleUnverifiableCodeE () = Message("PossibleUnverifiableCode", "")
let DeprecatedE () = Message("Deprecated", "%s")
let LibraryUseOnlyE () = Message("LibraryUseOnly", "")
Expand Down Expand Up @@ -1789,13 +1790,21 @@ type Exception with

| ValNotLocal _ -> os.AppendString(ValNotLocalE().Format)

| ObsoleteDiagnostic(message = s) ->
| ObsoleteDiagnostic(message = message) ->
os.AppendString(Obsolete1E().Format)

if s <> "" then
os.AppendString(Obsolete2E().Format s)
match message with
| Some message when message <> "" -> os.AppendString(Obsolete2E().Format message)
| _ -> ()

| Experimental(message = message) ->
os.AppendString(Experimental1E().Format)

match message with
| Some message when message <> "" -> os.AppendString(Experimental2E().Format message)
| _ -> ()

| Experimental(s, _) -> os.AppendString(ExperimentalE().Format s)
os.AppendString(Experimental3E().Format)

| PossibleUnverifiableCode _ -> os.AppendString(PossibleUnverifiableCodeE().Format)

Expand Down
10 changes: 8 additions & 2 deletions src/Compiler/FSStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,14 @@
<data name="Obsolete2" xml:space="preserve">
<value>. {0}</value>
</data>
<data name="Experimental" xml:space="preserve">
<value>{0}. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.</value>
<data name="Experimental1" xml:space="preserve">
<value>This construct is experimental</value>
</data>
<data name="Experimental2" xml:space="preserve">
<value>. {0}</value>
</data>
<data name="Experimental3" xml:space="preserve">
<value>. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.</value>
</data>
<data name="PossibleUnverifiableCode" xml:space="preserve">
<value>Uses of this construct may result in the generation of unverifiable .NET IL code. This warning can be disabled using '--nowarn:9' or '#nowarn "9"'.</value>
Expand Down
10 changes: 9 additions & 1 deletion src/Compiler/Facilities/DiagnosticsLogger.fs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ exception LibraryUseOnly of range: range

exception Deprecated of message: string * range: range

exception Experimental of message: string * range: range
exception Experimental of message: string option * diagnosticId: string option * urlFormat: string option * range: range

exception PossibleUnverifiableCode of range: range

Expand All @@ -133,6 +133,14 @@ exception DiagnosticWithSuggestions of number: int * message: string * range: ra
/// A diagnostic that is raised when enabled manually, or by default with a language feature
exception DiagnosticEnabledWithLanguageFeature of number: int * message: string * range: range * enabledByLangFeature: bool

/// A diagnostic that is raised when a diagnostic is obsolete
exception ObsoleteDiagnostic of
isError: bool *
diagnosticId: string option *
message: string option *
urlFormat: string option *
range: range

/// The F# compiler code currently uses 'Error(...)' in many places to create
/// an DiagnosticWithText as an exception even if it's a warning.
///
Expand Down
9 changes: 8 additions & 1 deletion src/Compiler/Facilities/DiagnosticsLogger.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ exception LibraryUseOnly of range: range

exception Deprecated of message: string * range: range

exception Experimental of message: string * range: range
exception Experimental of message: string option * diagnosticId: string option * urlFormat: string option * range: range

exception PossibleUnverifiableCode of range: range

Expand All @@ -87,6 +87,13 @@ exception DiagnosticWithSuggestions of
identifier: string *
suggestions: Suggestions

exception ObsoleteDiagnostic of
isError: bool *
diagnosticId: string option *
message: string option *
urlFormat: string option *
range: range

/// Creates a DiagnosticWithSuggestions whose text comes via SR.*
val ErrorWithSuggestions: (int * string) * range * string * Suggestions -> exn

Expand Down
Loading