1+ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+ namespace FSharp.Compiler.SourceCodeServices
4+
5+ open FSharp.Compiler .AbstractIL .Internal .Library
6+
7+ module PathUtils =
8+ open System.IO
9+ //[<Sealed>]
10+ //type Path =
11+ // static member GetFullPathSafe path =
12+ // try Path.GetFullPath path
13+ // with _ -> path
14+ //
15+ // static member GetFileNameSafe path =
16+ // try Path.GetFileName path
17+ // with _ -> path
18+
19+ let (</>) a b = Path.Combine( a, b)
20+
21+ module HashDirectiveInfo =
22+ open System.IO
23+ open PathUtils
24+ open FSharp.Compiler .Range
25+ open FSharp.Compiler .Ast
26+
27+ type IncludeDirective =
28+ | ResolvedDirectory of string
29+
30+ type CodeStringLiteral = string
31+
32+ type LoadFile =
33+ | Existing of CodeStringLiteral
34+ | Unresolvable of CodeStringLiteral
35+
36+ type LoadToken = CodeStringLiteral
37+
38+ type LoadDirective = { token: LoadToken ; files : LoadFile list }
39+
40+ [<NoComparison>]
41+ type Directive =
42+ | Include of IncludeDirective * range
43+ | Load of LoadDirective * range
44+
45+ // notes:
46+ // #I encountered in other loaded scripts may have an impact and this code may not resolve the same
47+ // /!\ @dsyme note https://github.com/Microsoft/visualfsharp/pull/4122#issuecomment-430983774 /!\
48+ // This looks like a partial reimplementation of aspects of the resolution logic done by the main F# compiler code.
49+ // The normal approach to this would be to record the resolutions in from the type-checking/analysis phase and report those resolutions here, rather than reimplementing the resolution logic.
50+ // In this case it's not a big problem. But there's a bit of trend toward reimplementing core compiler logic under src/fsharp/service and of course in the long term that will be a maintenance problem.
51+
52+ /// returns an array of LoadScriptResolutionEntries
53+ /// based on #I and #load directives
54+ let getIncludeAndLoadDirectives ast =
55+ // the Load items are resolved using fallback resolution relying on previously parsed #I directives
56+ // (this behaviour is undocumented in F# but it seems to be how it works).
57+
58+ // list of #I directives so far (populated while encountering those in order)
59+ let pushInclude , tryFindInPathsIncludedSoFar =
60+ let includesSoFar = ResizeArray<_>()
61+
62+ includesSoFar.Add,
63+ fun fileName ->
64+ includesSoFar
65+ |> Seq.tryPick ( fun ( ResolvedDirectory d ) ->
66+ let filePath = d </> fileName
67+ if FileSystem.SafeExists filePath then
68+ Some filePath
69+ else
70+ None
71+ )
72+
73+ let getDirectoryOfFile = FileSystem.GetFullPathSafe >> FileSystem.GetDirectoryNameShim
74+ let makeRootedDirectoryIfNecessary baseDirectory directory =
75+ if not ( FileSystem.IsPathRootedShim directory) then
76+ FileSystem.GetFullPathSafe ( baseDirectory </> directory)
77+ else
78+ directory
79+
80+ let parseDirectives modules file = [|
81+ let baseDirectory = getDirectoryOfFile file
82+ for ( SynModuleOrNamespace (_, _, _, declarations, _, _, _, _)) in modules do
83+ for decl in declarations do
84+ match decl with
85+ | SynModuleDecl.HashDirective ( ParsedHashDirective( " I" ,[ directory], range),_) ->
86+ let directory = makeRootedDirectoryIfNecessary ( getDirectoryOfFile file) directory
87+
88+ if FileSystem.DirectoryExistsShim directory then
89+ let includeDirective = ResolvedDirectory( directory)
90+ pushInclude includeDirective
91+ yield Include ( includeDirective, range)
92+
93+ | SynModuleDecl.HashDirective ( ParsedHashDirective ( " load" , files, range),_) ->
94+ for f in files do
95+ if FileSystem.IsPathRootedShim f && FileSystem.SafeExists f then
96+ // this is absolute reference to an existing script, easiest case
97+ yield Load ({ token = " " ; files = [ Existing f]}, range)
98+ else
99+ // I'm not sure if the order is correct, first checking relative to file containing the #load directive
100+ // then checking for undocumented resolution using previously parsed #I directives
101+ let fileRelativeToCurrentFile = baseDirectory </> f
102+ if FileSystem.SafeExists fileRelativeToCurrentFile then
103+ // this is existing file relative to current file
104+ yield Load ({ token = " " ; files = [ Existing fileRelativeToCurrentFile]}, range)
105+ else
106+ // match file against first include which seemingly have it found
107+ match tryFindInPathsIncludedSoFar f with
108+ | None -> () // can't load this file even using any of the #I directives...
109+ | Some f -> yield Load ({ token = " " ; files = [ Existing f]}, range)
110+ | _ -> ()
111+ |]
112+
113+ match ast with
114+ | ParsedInput.ImplFile ( ParsedImplFileInput( fn,_,_,_,_, modules,_)) -> parseDirectives modules fn
115+ | _ -> [||]
116+
117+ /// returns the Some (complete file name of a resolved #load directive at position) or None
118+ let getHashLoadDirectiveResolvedPathAtPosition ( pos : pos ) ( ast : ParsedInput ) : string option =
119+ getIncludeAndLoadDirectives ast
120+ |> Array.tryPick (
121+ function
122+ | Load ({ token = " " ; files = [ Existing f] }, range)
123+ // check the line is within the range
124+ // todo: doesn't work when there are multiple files given to a single #load directive
125+ when rangeContainsPos range pos
126+ -> Some f
127+ | _ -> None
128+ )
0 commit comments