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 preliminary support for Mapped Types #122

Closed
wants to merge 9 commits into from

Conversation

roboz0r
Copy link
Contributor

@roboz0r roboz0r commented Sep 15, 2024

declare const NODES: readonly ["a", "button"];
type Primitives = {
    [E in (typeof NODES)[number]]: boolean;
};

transforms to

[<AllowNullLiteral>]
[<Interface>]
type Primitives =
    abstract member a: bool with get, set
    abstract member button: bool with get, set

Summary

  • Adds new GlueType.MappedType
  • Adds ObjectType to IndexedAccessType
  • Adds corresponding transform functions
  • Adds SyntaxKind.name helper function for debugging
  • Adds __SOURCE_FILE__ literals to errors for debugging

Copy link
Contributor

@MangelMaxime MangelMaxime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for start the work.

I clean up a little the code, made it more type safe but I think overall it looks fine.

I will also have a look to improve the error message because now with the change introduced in this PR, they are not all on par..

src/Glutinum.Converter/Reader/MappedTypeNode.fs Outdated Show resolved Hide resolved
src/Glutinum.Converter/Transform.fs Outdated Show resolved Hide resolved
src/Glutinum.Converter/GlueAST.fs Outdated Show resolved Hide resolved
@roboz0r
Copy link
Contributor Author

roboz0r commented Sep 15, 2024

I will also have a look to improve the error message because now with the change introduced in this PR, they are not all on par..

If you're happy with my changes to the error reporting I'll go through and add SyntaxKind.name and __SOURCE_FILE__ into the relevant locations. I was primarily thinking of myself when running the tests and going back and forth between the enum list and trying to work out the file where the error originated.

@roboz0r
Copy link
Contributor Author

roboz0r commented Sep 15, 2024

I added SyntaxKind.name in the relevant error messages that I found, I also added __SOURCE_FILE__ to errors that aren't throwing exceptions as the exception would contain source information anyway.

@MangelMaxime
Copy link
Contributor

I was primarily thinking of myself when running the tests and going back and forth between the enum list and trying to work out the file where the error originated.

My way of doing it was by copy / searching the context error across the file. But having the name of the source file, is either because in general editors allow you to fuzzy search by filename in general.

It is just a shame that we can't do something like:

module Log =

    let inline info (message: string) =
        printfn $"%s{__SOURCE_FILE__}: %s{message}"

Where __SOURCE_FILE__ is from the caller thanks to inlining the function

Comment on lines +155 to +232
let private readTypeQuery
(reader: ITypeScriptReader)
(typeNodeQuery: Ts.TypeQueryNode)
=
let checker = reader.checker
let typ = checker.getTypeAtLocation !!typeNodeQuery.exprName

resultOption {
// This is safe as both cases have a `kind` field
let exprNameKind: Ts.SyntaxKind = typeNodeQuery.exprName?kind
// This logic is highly overlapping with readTypeUsingFlags
// It should be refactored to achieve a sane flow
match typ.flags, typ.getSymbol (), exprNameKind with
| HasTypeFlags Ts.TypeFlags.Object, None, Ts.SyntaxKind.Identifier ->
let exprName: Ts.Identifier = !!typeNodeQuery.exprName

let! aliasSymbol =
checker.getSymbolAtLocation exprName
|> ResultOption.ofOption
|> Result.mapError (fun _ ->
generateReaderError
"type node (TypeQuery)"
"Missing symbol"
typeNodeQuery
)

let! declaration =
aliasSymbol.declarations
|> Option.map (fun declarations ->
if declarations.Count <> 1 then
None
else
Some declarations.[0]
)
|> ResultOption.ofOption
|> Result.mapError (fun _ ->
generateReaderError
"type node (TypeQuery)"
"Expected exactly one declaration"
typeNodeQuery
)

let! variableDeclaration =
declaration
|> Option.bind (fun declaration ->
match declaration.kind with
| Ts.SyntaxKind.VariableDeclaration ->
Some(declaration :?> Ts.VariableDeclaration)
| _ -> None
)
|> ResultOption.ofOption
|> Result.mapError (fun _ ->
generateReaderError
"type node (TypeQuery)"
"Unsupported declaration kind"
typeNodeQuery
)

let! typeNode =
variableDeclaration.``type``
|> ResultOption.ofOption
|> Result.mapError (fun _ ->
generateReaderError
"type node (TypeQuery)"
"Missing type"
typeNodeQuery
)

match typeNode.kind with
| Ts.SyntaxKind.TypeOperator ->
let typeOperatorNode = typeNode :?> Ts.TypeOperatorNode

return reader.ReadTypeOperatorNode typeOperatorNode

| _ -> return readTypeUsingFlags reader typ

| _ -> return readTypeUsingFlags reader typ
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roboz0r

I gave another chance to rewrite the TypeQuery code. Do you think this version of the code is easier to read?

Personally, I think it is easier to read because it remove (at least visually) several level of pattern matching over options.

Copy link
Contributor Author

@roboz0r roboz0r Sep 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to use the CE to flatten the nested pattern matching but I'd really like to be able to combine and refactor this function and readTypeUsingFlags to avoid the overlapping logical branches. I couldn't work out an easy way to do it yesterday but if you think it's a good idea too, I'll give it another go.

Maybe it should also be split off into a separate Reader/TypeQueryNode.fs file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to use the CE to flatten the nested pattern matching but I'd really like to be able to combine and refactor this function and readTypeUsingFlags to avoid the overlapping logical branches. I couldn't work out an easy way to do it yesterday but if you think it's a good idea too, I'll give it another go.

I didn't look too much on that, but if you can refactor the code to have a single function/path I am all for it. With or without the CE depending on what works best.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MangelMaxime I came up with an alternative rewrite of this code here 031dcd5 I found that just result {} rather than resultOption was sufficient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments on your commit but I think we are on the good track. If we fix the comments, we should be able to merge it in Glutinum and keep moving forward.

@MangelMaxime
Copy link
Contributor

I also gave a chance to re-think the error reporter:

let generateReaderError
    (fileFolder: string, fileName: string, fileLine: string)
    (errorContext: string)
    (reason: string)
    (node: Ts.Node)
    =
    let sourceFile = node.getSourceFile ()
    let lineAndChar = sourceFile.getLineAndCharacterOfPosition node.pos
    let line = int lineAndChar.line + 1
    let column = int lineAndChar.character + 1

    let errorOrigin =
        let fileFolder =
            let index = fileFolder.IndexOf("src/Glutinum.Converter")
            "./" + fileFolder.Substring(index)

        $"%s{fileFolder}/%s{fileName}(%s{fileLine})"

    $"""%s{errorOrigin}: Error while reading %s{errorContext} from:
%s{sourceFile.fileName}(%d{line},%d{column})

%s{reason}

--- Text ---
%s{node.getFullText ()}
---

--- Parent text ---
%s{node.parent.getFullText ()}
---"""

Example of output:

./src/Glutinum.Converter/Reader/TypeNode.fs(184): Error while reading type node (TypeQuery) from:
/Users/mmangel/Workspaces/Github/glutinum-org/Glutinum.Converter/tests/specs/references/typeof/typeofStrings.d.ts(3,12)

Missing symbol

--- Text ---
typeof NODES
---

--- Parent text ---
 (typeof NODES)
---

This allows to ensure that all errors have the same level of information, because it can be easy to forget to add in {__SOURCE_FILE__}.

I decided to use a relative path for reporting the origin of the error because it seems like for VSCode it allows to click on the link to go the file declaration.

If others IDEs have don't support that, we can do something like:

    let errorOrigin =
        let fileFolder =
        #if DEBUG
            $"%s{fileFolder}"
        #else
            let index = fileFolder.IndexOf("src/Glutinum.Converter")
            "./" + fileFolder.Substring(index)
        #endif

like that in DEBUG we generate a full path and in release we have a relative path. The relative path in release, is because showing my system architecture doesn't matter and add noise for no reason.

What do you think? If you agree on this proposition, I can make the change to the PR

@roboz0r
Copy link
Contributor Author

roboz0r commented Sep 15, 2024

Where __SOURCE_FILE__ is from the caller thanks to inlining the function

In .NET land we could use [<CallerFilePath>] but I expect that doesn't work with Fable.

@roboz0r
Copy link
Contributor Author

roboz0r commented Sep 15, 2024

re-think the error reporter

proposed error format looks good. Just a note to normalize \ to / before processing the paths. Unfortunately it seems like the System.IO.Path methods haven't been ported over to Fable.

@MangelMaxime
Copy link
Contributor

Where __SOURCE_FILE__ is from the caller thanks to inlining the function

In .NET land we could use [<CallerFilePath>] but I expect that doesn't work with Fable.

You would be surprised at how many times I have been surprised by Fable. And you can add +1 to that number because [< CallerFilePath>] works 🚀.

Thanks you for the tips, you have now idea for how long I have been looking for something like that 😅 (probably 5 years). It is really useful, when generating documentation with link to the source code.

I just need to rewrite the generateReaderError from a function to a member. I will probably create a Reporter type like that it will be able to host things like error, warning, info, etc. if we need to extends it later.

re-think the error reporter

proposed error format looks good. Just a note to normalize \ to / before processing the paths. Unfortunately it seems like the System.IO.Path methods haven't been ported over to Fable.

Nothing that a good old Replace can't fix 😅

@MangelMaxime
Copy link
Contributor

@roboz0r This PR was starting to be a bit messy with the different changes between the Reader + logger improvements.

I think it is best if you can open a new PR using the latest version of main in which I added the improvements we talked regarding the logger.

@roboz0r
Copy link
Contributor Author

roboz0r commented Sep 17, 2024

New PR #124

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants