|
| 1 | +module Tests.Service.SyntaxTree |
| 2 | + |
| 3 | +open System.IO |
| 4 | +open FSharp.Compiler.CodeAnalysis |
| 5 | +open FSharp.Compiler.Service.Tests.Common |
| 6 | +open FSharp.Compiler.Syntax |
| 7 | +open FSharp.Compiler.Text |
| 8 | +open NUnit.Framework |
| 9 | + |
| 10 | +let testCasesDir = Path.Combine(__SOURCE_DIRECTORY__, "data", "SyntaxTree") |
| 11 | + |
| 12 | +let allTestCases = |
| 13 | + Directory.EnumerateFiles(testCasesDir, "*.fs?", SearchOption.AllDirectories) |
| 14 | + |> Seq.map (fun f -> |
| 15 | + let fileInfo = FileInfo(f) |
| 16 | + let fileName = Path.Combine(fileInfo.Directory.Name, fileInfo.Name) |
| 17 | + [| fileName :> obj |]) |
| 18 | + |> Seq.toArray |
| 19 | + |
| 20 | +[<Literal>] |
| 21 | +let RootDirectory = @"/root" |
| 22 | + |
| 23 | +/// <summary> |
| 24 | +/// Everytime `__SOURCE_DIRECTORY__` was used in the code, the ast will contain an invalid value and range for it. |
| 25 | +/// This should be cleaned up when the test runs during CI/CD. |
| 26 | +/// </summary> |
| 27 | +/// <remarks> |
| 28 | +/// This function is incomplete and does not clean up the entire ParsedInput. |
| 29 | +/// A shortcut was made to only support the existing use-cases. |
| 30 | +/// </remarks> |
| 31 | +let private sanitizeAST (sourceDirectoryValue: string) (ast: ParsedInput) : ParsedInput = |
| 32 | + let isZero (m: range) = |
| 33 | + m.StartLine = 0 && m.StartColumn = 0 && m.EndLine = 0 && m.EndColumn = 0 |
| 34 | + |
| 35 | + // __SOURCE_DIRECTORY__ will contain the evaluated value, so we want to replace it with a stable value instead. |
| 36 | + let mapParsedHashDirective (ParsedHashDirective(ident, args, _) as phd) = |
| 37 | + match args with |
| 38 | + | [ ParsedHashDirectiveArgument.SourceIdentifier("__SOURCE_DIRECTORY__", _, mSourceDirectory) ] -> |
| 39 | + let mZero = |
| 40 | + Range.mkRange mSourceDirectory.FileName (Position.mkPos 0 0) (Position.mkPos 0 0) |
| 41 | + |
| 42 | + ParsedHashDirective( |
| 43 | + ident, |
| 44 | + [ ParsedHashDirectiveArgument.SourceIdentifier("__SOURCE_DIRECTORY__", sourceDirectoryValue, mZero) ], |
| 45 | + mZero |
| 46 | + ) |
| 47 | + | _ -> phd |
| 48 | + |
| 49 | + let (|SourceDirectoryConstant|_|) (constant: SynConst) = |
| 50 | + match constant with |
| 51 | + | SynConst.SourceIdentifier("__SOURCE_DIRECTORY__", _, mSourceDirectory) -> |
| 52 | + let mZero = |
| 53 | + Range.mkRange mSourceDirectory.FileName (Position.mkPos 0 0) (Position.mkPos 0 0) |
| 54 | + |
| 55 | + Some(SynConst.SourceIdentifier("__SOURCE_DIRECTORY__", sourceDirectoryValue, mZero), mZero) |
| 56 | + | _ -> None |
| 57 | + |
| 58 | + let (|SourceDirectoryConstantExpr|_|) (expr: SynExpr) = |
| 59 | + match expr with |
| 60 | + | SynExpr.Const(SourceDirectoryConstant(constant, mZero), _) -> Some(SynExpr.Const(constant, mZero)) |
| 61 | + | _ -> None |
| 62 | + |
| 63 | + let rec mapSynModuleDecl (mdl: SynModuleDecl) = |
| 64 | + match mdl with |
| 65 | + | SynModuleDecl.HashDirective(ParsedHashDirective(range = mZero) as hd, m) -> |
| 66 | + let hd = mapParsedHashDirective hd |
| 67 | + // Only update the range of SynModuleSigDecl.HashDirective if the value was updated. |
| 68 | + let m = if isZero mZero then mZero else m |
| 69 | + SynModuleDecl.HashDirective(hd, m) |
| 70 | + | SynModuleDecl.NestedModule(moduleInfo, isRecursive, decls, isContinuing, range, trivia) -> |
| 71 | + SynModuleDecl.NestedModule(moduleInfo, isRecursive, List.map mapSynModuleDecl decls, isContinuing, range, trivia) |
| 72 | + | SynModuleDecl.Expr(SourceDirectoryConstantExpr(expr), _) -> SynModuleDecl.Expr(expr, expr.Range) |
| 73 | + | _ -> mdl |
| 74 | + |
| 75 | + let mapSynModuleOrNamespace (SynModuleOrNamespace(longId, isRecursive, kind, decls, xmlDoc, attribs, ao, range, trivia)) = |
| 76 | + SynModuleOrNamespace(longId, isRecursive, kind, List.map mapSynModuleDecl decls, xmlDoc, attribs, ao, range, trivia) |
| 77 | + |
| 78 | + let rec mapSynModuleDeclSig (msdl: SynModuleSigDecl) = |
| 79 | + match msdl with |
| 80 | + | SynModuleSigDecl.HashDirective(ParsedHashDirective(range = mZero) as hd, m) -> |
| 81 | + let hd = mapParsedHashDirective hd |
| 82 | + // Only update the range of SynModuleSigDecl.HashDirective if the value was updated. |
| 83 | + let m = if isZero mZero then mZero else m |
| 84 | + SynModuleSigDecl.HashDirective(hd, m) |
| 85 | + | SynModuleSigDecl.NestedModule(moduleInfo, isRecursive, decls, range, trivia) -> |
| 86 | + SynModuleSigDecl.NestedModule(moduleInfo, isRecursive, List.map mapSynModuleDeclSig decls, range, trivia) |
| 87 | + | _ -> msdl |
| 88 | + |
| 89 | + let mapSynModuleOrNamespaceSig (SynModuleOrNamespaceSig(longId, isRecursive, kind, decls, xmlDoc, attribs, ao, range, trivia)) = |
| 90 | + SynModuleOrNamespaceSig(longId, isRecursive, kind, List.map mapSynModuleDeclSig decls, xmlDoc, attribs, ao, range, trivia) |
| 91 | + |
| 92 | + match ast with |
| 93 | + | ParsedInput.ImplFile(ParsedImplFileInput(fileName, |
| 94 | + isScript, |
| 95 | + qualifiedNameOfFile, |
| 96 | + scopedPragmas, |
| 97 | + hashDirectives, |
| 98 | + contents, |
| 99 | + flags, |
| 100 | + trivia, |
| 101 | + identifiers)) -> |
| 102 | + ParsedImplFileInput( |
| 103 | + fileName, |
| 104 | + isScript, |
| 105 | + qualifiedNameOfFile, |
| 106 | + scopedPragmas, |
| 107 | + List.map mapParsedHashDirective hashDirectives, |
| 108 | + List.map mapSynModuleOrNamespace contents, |
| 109 | + flags, |
| 110 | + trivia, |
| 111 | + identifiers |
| 112 | + ) |
| 113 | + |> ParsedInput.ImplFile |
| 114 | + | ParsedInput.SigFile(ParsedSigFileInput(fileName, qualifiedNameOfFile, scopedPragmas, hashDirectives, contents, trivia, identifiers)) -> |
| 115 | + ParsedSigFileInput( |
| 116 | + fileName, |
| 117 | + qualifiedNameOfFile, |
| 118 | + scopedPragmas, |
| 119 | + List.map mapParsedHashDirective hashDirectives, |
| 120 | + List.map mapSynModuleOrNamespaceSig contents, |
| 121 | + trivia, |
| 122 | + identifiers |
| 123 | + ) |
| 124 | + |> ParsedInput.SigFile |
| 125 | + |
| 126 | +let parseSourceCode (name: string, code: string) = |
| 127 | + let location = Path.Combine(RootDirectory, name).Replace("\\", "/") |
| 128 | + |
| 129 | + let parseResults = |
| 130 | + checker.ParseFile( |
| 131 | + location, |
| 132 | + SourceText.ofString code, |
| 133 | + { FSharpParsingOptions.Default with |
| 134 | + SourceFiles = [| location |] } |
| 135 | + ) |
| 136 | + |> Async.RunImmediate |
| 137 | + |
| 138 | + let tree = parseResults.ParseTree |
| 139 | + let sourceDirectoryValue = $"{RootDirectory}/{FileInfo(location).Directory.Name}" |
| 140 | + sanitizeAST sourceDirectoryValue tree |
| 141 | + |
| 142 | +/// Asserts the parsed untyped tree matches the expected baseline. |
| 143 | +/// |
| 144 | +/// To update a baseline: |
| 145 | +/// CMD: set TEST_UPDATE_BSL=1 & dotnet test --filter "ParseFile" |
| 146 | +/// PowerShell: $env:TEST_UPDATE_BSL = "1" ; dotnet test --filter "ParseFile" |
| 147 | +/// Linux/macOS: export TEST_UPDATE_BSL=1 & dotnet test --filter "ParseFile" |
| 148 | +/// |
| 149 | +/// Assuming your current directory is tests/FSharp.Compiler.Service.Tests |
| 150 | +[<TestCaseSource(nameof allTestCases)>] |
| 151 | +let ParseFile fileName = |
| 152 | + let fullPath = Path.Combine(testCasesDir, fileName) |
| 153 | + let contents = File.ReadAllText fullPath |
| 154 | + let ast = parseSourceCode (fileName, contents) |
| 155 | + let normalize (s: string) = s.Replace("\r", "") |
| 156 | + let actual = sprintf "%A" ast |> normalize |> sprintf "%s\n" |
| 157 | + let bslPath = $"{fullPath}.bsl" |
| 158 | + |
| 159 | + let expected = |
| 160 | + if File.Exists bslPath then |
| 161 | + File.ReadAllText bslPath |> normalize |
| 162 | + else |
| 163 | + "No baseline was found" |
| 164 | + |
| 165 | + let testUpdateBSLEnv = System.Environment.GetEnvironmentVariable("TEST_UPDATE_BSL") |
| 166 | + |
| 167 | + if not (isNull testUpdateBSLEnv) && testUpdateBSLEnv.Trim() = "1" then |
| 168 | + File.WriteAllText(bslPath, actual) |
| 169 | + |
| 170 | + Assert.AreEqual(expected, actual) |
0 commit comments