diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 02136c45fd0..2207a4792eb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -233,6 +233,19 @@ jobs: - script: eng\CIBuild.cmd -configuration Release -noSign /p:DotNetBuildFromSource=true /p:FSharpSourceBuild=true displayName: Build + # Up-to-date + - job: UpToDate_Windows + pool: + vmImage: windows-2019 + steps: + - checkout: self + clean: true + - task: PowerShell@2 + displayName: Run up-to-date build check + inputs: + filePath: eng\tests\UpToDate.ps1 + arguments: -configuration $(_BuildConfig) -ci -binaryLog + #---------------------------------------------------------------------------------------------------------------------# # FCS builds # #---------------------------------------------------------------------------------------------------------------------# diff --git a/eng/tests/UpToDate.ps1 b/eng/tests/UpToDate.ps1 new file mode 100644 index 00000000000..a7e50a26d60 --- /dev/null +++ b/eng/tests/UpToDate.ps1 @@ -0,0 +1,70 @@ +# This script verifies that subsequent calls to `Build.cmd` don't cause assemblies to be unnecessarily rebuilt. + +[CmdletBinding(PositionalBinding=$false)] +param ( + [string][Alias('c')]$configuration = "Debug", + [parameter(ValueFromRemainingArguments=$true)][string[]]$properties +) + +Set-StrictMode -version 2.0 +$ErrorActionPreference = "Stop" + +try { + $RepoRoot = Join-Path $PSScriptRoot ".." | Join-Path -ChildPath ".." -Resolve + $BuildScript = Join-Path $RepoRoot "Build.cmd" + + # do first build + & $BuildScript -configuration $configuration @properties + if ($LASTEXITCODE -ne 0) { + Write-Host "Error running first build." + exit 1 + } + + # gather assembly timestamps + $ArtifactsBinDir = Join-Path $RepoRoot "artifacts" | Join-Path -ChildPath "bin" -Resolve + $FSharpAssemblyDirs = Get-ChildItem -Path $ArtifactsBinDir -Filter "FSharp.*" + $FSharpAssemblyPaths = $FSharpAssemblyDirs | ForEach-Object { Get-ChildItem -Path (Join-Path $ArtifactsBinDir $_) -Recurse -Filter "$_.dll" } | ForEach-Object { $_.FullName } + + $InitialAssembliesAndTimes = @{} + foreach ($asm in $FSharpAssemblyPaths) { + $LastWriteTime = (Get-Item $asm).LastWriteTimeUtc + $InitialAssembliesAndTimes.Add($asm, $LastWriteTime) + } + + $InitialCompiledCount = $FSharpAssemblyPaths.Length + + # build again + & $BuildScript -configuration $configuration @properties + if ($LASTEXITCODE -ne 0) { + Write-Host "Error running second build." + exit 1 + } + + # gather assembly timestamps again + $FinalAssembliesAndTimes = @{} + foreach ($asm in $FSharpAssemblyPaths) { + $LastWriteTime = (Get-Item $asm).LastWriteTimeUtc + $FinalAssembliesAndTimes.Add($asm, $LastWriteTime) + } + + # validate that assembly timestamps haven't changed + $RecompiledFiles = @() + foreach ($asm in $InitialAssembliesAndTimes.keys) { + $InitialTime = $InitialAssembliesAndTimes[$asm] + $FinalTime = $FinalAssembliesAndTimes[$asm] + if ($InitialTime -ne $FinalTime) { + $RecompiledFiles += $asm + } + } + + $RecompiledCount = $RecompiledFiles.Length + Write-Host "$RecompiledCount of $InitialCompiledCount assemblies were re-compiled" + $RecompiledFiles | ForEach-Object { Write-Host " $_" } + exit $RecompiledCount +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + exit 1 +} diff --git a/src/fsharp/service/ServiceAnalysis.fs b/src/fsharp/service/ServiceAnalysis.fs index f66d372bc28..e4f942b657a 100644 --- a/src/fsharp/service/ServiceAnalysis.fs +++ b/src/fsharp/service/ServiceAnalysis.fs @@ -99,17 +99,27 @@ module UnusedOpens = | :? FSharpMemberOrFunctionOrValue as fv when fv.IsExtensionMember -> // Extension members should be taken into account even though they have a prefix (as they do most of the time) true + | :? FSharpMemberOrFunctionOrValue as fv when not fv.IsModuleValueOrMember -> // Local values can be ignored false + | :? FSharpMemberOrFunctionOrValue when su.IsFromDefinition -> // Value definitions should be ignored false + | :? FSharpGenericParameter -> // Generic parameters can be ignored, they never come into scope via 'open' false + | :? FSharpUnionCase when su.IsFromDefinition -> false + + | :? FSharpField as field when + field.DeclaringEntity.IsSome && field.DeclaringEntity.Value.IsFSharpRecord -> + // Record fields are used in name resolution + true + | _ -> // For the rest of symbols we pick only those which are the first part of a long ident, because it's they which are // contained in opened namespaces / modules. For example, we pick `IO` from long ident `IO.File.OpenWrite` because diff --git a/src/fsharp/symbols/SymbolPatterns.fs b/src/fsharp/symbols/SymbolPatterns.fs index 2265682692b..6c2286b4095 100644 --- a/src/fsharp/symbols/SymbolPatterns.fs +++ b/src/fsharp/symbols/SymbolPatterns.fs @@ -132,8 +132,8 @@ module Symbol = #endif let (|Enum|_|) (entity: FSharpEntity) = if entity.IsEnum then Some() else None - let (|Tuple|_|) (ty: FSharpType option) = - ty |> Option.bind (fun ty -> if ty.IsTupleType then Some() else None) + let (|Tuple|_|) (ty: FSharpType) = + if ty.IsTupleType then Some() else None let (|RefCell|_|) (ty: FSharpType) = match getAbbreviatedType ty with diff --git a/src/fsharp/symbols/SymbolPatterns.fsi b/src/fsharp/symbols/SymbolPatterns.fsi index c4c242270cd..ca3659f34d6 100644 --- a/src/fsharp/symbols/SymbolPatterns.fsi +++ b/src/fsharp/symbols/SymbolPatterns.fsi @@ -37,7 +37,7 @@ module public Symbol = val (|ProvidedAndErasedType|_|) : FSharpEntity -> unit option #endif val (|Enum|_|) : FSharpEntity -> unit option - val (|Tuple|_|) : FSharpType option -> unit option + val (|Tuple|_|) : FSharpType -> unit option val (|RefCell|_|) : FSharpType -> unit option val (|FunctionType|_|) : FSharpType -> unit option val (|Pattern|_|) : FSharpSymbol -> unit option diff --git a/tests/service/ProjectAnalysisTests.fs b/tests/service/ProjectAnalysisTests.fs index 08f19cbe708..9dca0cd56f6 100644 --- a/tests/service/ProjectAnalysisTests.fs +++ b/tests/service/ProjectAnalysisTests.fs @@ -5523,6 +5523,14 @@ type UseTheThings(i:int) = member x.UseSomeUsedModuleContainingActivePattern(ActivePattern g) = g member x.UseSomeUsedModuleContainingExtensionMember() = (3).Q member x.UseSomeUsedModuleContainingUnion() = A + +module M1 = + type R = { Field: int } + +module M2 = + open M1 + + let foo x = x.Field """ let fileSource1 = FSharp.Compiler.Text.SourceText.ofString fileSource1Text File.WriteAllText(fileName1, fileSource1Text)