• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

forst-lang / forst / 16707212781

03 Aug 2025 04:41PM UTC coverage: 38.599% (-2.5%) from 41.086%
16707212781

Pull #30

github

haveyaseen
feat: Improve TS signature collection & codegen
Pull Request #30: feat: Generate TypeScript types

1542 of 4988 new or added lines in 69 files covered. (30.91%)

184 existing lines in 23 files now uncovered.

6487 of 16806 relevant lines covered (38.6%)

7.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.56
/forst/internal/parser/function.go
1
package parser
2

3
import (
4
        "forst/internal/ast"
5
)
6

7
func (p *Parser) parseParameterType() ast.TypeNode {
×
8
        next := p.peek()
×
9
        // TODO: Even if the next token is a dot this could be a type without any constraints
×
10
        if next.Type == ast.TokenDot || next.Type == ast.TokenLParen {
×
11
                assertion := p.parseAssertionChain(true)
×
12
                return ast.TypeNode{
×
13
                        Ident:     ast.TypeAssertion,
×
14
                        Assertion: &assertion,
×
15
                }
×
16
        }
×
17
        // Disallow Shape({...}) wrapper for shape types
18
        if p.current().Type == ast.TokenIdentifier && p.current().Value == "Shape" {
×
19
                ident := p.expect(ast.TokenIdentifier)
×
20
                if p.current().Type == ast.TokenLParen {
×
21
                        p.FailWithParseError(p.current(), "Shape({...}) wrapper is not allowed. Use {...} directly for shape types.")
×
22
                }
×
23
                return ast.TypeNode{
×
24
                        Ident: ast.TypeIdent(ident.Value),
×
25
                }
×
26
        }
27
        // Allow direct {...} for shape types
28
        if p.current().Type == ast.TokenLBrace {
×
29
                shape := p.parseShapeType()
×
30
                baseType := ast.TypeIdent(ast.TypeShape)
×
31
                return ast.TypeNode{
×
32
                        Ident: ast.TypeShape,
×
33
                        Assertion: &ast.AssertionNode{
×
34
                                BaseType: &baseType,
×
35
                                Constraints: []ast.ConstraintNode{{
×
36
                                        Name: "Match",
×
37
                                        Args: []ast.ConstraintArgumentNode{{
×
38
                                                Shape: &shape,
×
39
                                        }},
×
40
                                }},
×
41
                        },
×
42
                }
×
43
        }
×
44
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
×
45
}
46

47
func (p *Parser) parseDestructuredParameter() ast.ParamNode {
×
48
        p.expect(ast.TokenLBrace)
×
49
        fields := []string{}
×
50

×
51
        // Parse fields until we hit closing brace
×
52
        for p.current().Type != ast.TokenRBrace {
×
53
                name := p.expect(ast.TokenIdentifier)
×
54
                fields = append(fields, name.Value)
×
55

×
56
                // Handle comma between fields
×
57
                if p.current().Type == ast.TokenComma {
×
58
                        p.advance()
×
59
                }
×
60
        }
61

62
        p.expect(ast.TokenRBrace)
×
63
        p.expect(ast.TokenColon)
×
64

×
65
        paramType := p.parseParameterType()
×
66

×
67
        return ast.DestructuredParamNode{
×
68
                Fields: fields,
×
69
                Type:   paramType,
×
70
        }
×
71
}
72

73
func (p *Parser) parseSimpleParameter() ast.ParamNode {
7✔
74
        name := p.expect(ast.TokenIdentifier).Value
7✔
75
        // Remove colon requirement - use Go-style parameter declarations
7✔
76

7✔
77
        tok := p.current()
7✔
78
        if tok.Type == ast.TokenIdentifier && (p.peek().Type == ast.TokenDot || p.peek().Type == ast.TokenLParen) {
8✔
79
                assertion := p.parseAssertionChain(true)
1✔
80
                return ast.SimpleParamNode{
1✔
81
                        Ident: ast.Ident{ID: ast.Identifier(name)},
1✔
82
                        Type: ast.TypeNode{
1✔
83
                                Ident:     ast.TypeAssertion,
1✔
84
                                Assertion: &assertion,
1✔
85
                        },
1✔
86
                }
1✔
87
        }
1✔
88

89
        if tok.Type == ast.TokenIdentifier && tok.Value == "Shape" {
6✔
90
                // Check if this is Shape({...})
×
91
                if p.peek().Type == ast.TokenLParen {
×
92
                        p.FailWithParseError(tok, "Direct usage of Shape({...}) is not allowed. Use a shape type directly, e.g. { field: Type }.")
×
93
                }
×
94
                // Allow direct usage of Shape as a type name
95
                p.advance()
×
96
                typeIdent := ast.TypeIdent("Shape")
×
97
                return ast.SimpleParamNode{
×
98
                        Ident: ast.Ident{ID: ast.Identifier(name)},
×
99
                        Type:  ast.TypeNode{Ident: typeIdent},
×
100
                }
×
101
        }
102
        if tok.Type == ast.TokenLBrace {
7✔
103
                shape := p.parseShapeType()
1✔
104
                baseType := ast.TypeIdent(ast.TypeShape)
1✔
105
                return ast.SimpleParamNode{
1✔
106
                        Ident: ast.Ident{ID: ast.Identifier(name)},
1✔
107
                        Type: ast.TypeNode{
1✔
108
                                Ident: ast.TypeShape,
1✔
109
                                Assertion: &ast.AssertionNode{
1✔
110
                                        BaseType: &baseType,
1✔
111
                                        Constraints: []ast.ConstraintNode{{
1✔
112
                                                Name: "Match",
1✔
113
                                                Args: []ast.ConstraintArgumentNode{{
1✔
114
                                                        Shape: &shape,
1✔
115
                                                }},
1✔
116
                                        }},
1✔
117
                                },
1✔
118
                        },
1✔
119
                }
1✔
120
        }
1✔
121
        // Parse the type, which may include dots (e.g. AppMutation.Input)
122
        typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
5✔
123
        p.logParsedNodeWithMessage(typ, "Parsed parameter type, next token: "+p.current().Type.String()+" ("+p.current().Value+")")
5✔
124
        return ast.SimpleParamNode{
5✔
125
                Ident: ast.Ident{ID: ast.Identifier(name)},
5✔
126
                Type:  typ,
5✔
127
        }
5✔
128
}
129

130
func (p *Parser) parseParameter() ast.ParamNode {
7✔
131
        switch p.current().Type {
7✔
132
        case ast.TokenIdentifier:
7✔
133
                return p.parseSimpleParameter()
7✔
134
        case ast.TokenLBrace:
×
135
                return p.parseDestructuredParameter()
×
136
        default:
×
137
                p.FailWithParseError(p.current(), "Expected parameter")
×
138
                panic("Reached unreachable path")
×
139
        }
140
}
141

142
// Parse function parameters
143
func (p *Parser) parseFunctionSignature() []ast.ParamNode {
34✔
144
        p.expect(ast.TokenLParen)
34✔
145
        params := []ast.ParamNode{}
34✔
146

34✔
147
        // Handle empty parameter list
34✔
148
        if p.current().Type == ast.TokenRParen {
62✔
149
                p.advance()
28✔
150
                return params
28✔
151
        }
28✔
152

153
        // Parse parameters
154
        for {
12✔
155
                param := p.parseParameter()
6✔
156
                switch param.(type) {
6✔
157
                case ast.DestructuredParamNode:
×
158
                        p.logParsedNodeWithMessage(param, "Parsed destructured function param")
×
159
                default:
6✔
160
                        p.logParsedNodeWithMessage(param, "Parsed function param")
6✔
161
                }
162
                params = append(params, param)
6✔
163

6✔
164
                // Check if there are more parameters
6✔
165
                if p.current().Type == ast.TokenComma {
6✔
166
                        p.advance()
×
167
                } else {
6✔
168
                        break
6✔
169
                }
170
        }
171

172
        p.expect(ast.TokenRParen)
6✔
173
        return params
6✔
174
}
175

176
func (p *Parser) parseReturnType() []ast.TypeNode {
34✔
177
        returnType := []ast.TypeNode{}
34✔
178
        if p.current().Type == ast.TokenColon {
41✔
179
                p.advance() // Consume the colon
7✔
180
                // Support both single and parenthesized multiple return types
7✔
181
                if p.current().Type == ast.TokenLParen {
7✔
182
                        p.advance() // Consume '('
×
183
                        for {
×
NEW
184
                                typ := p.parseReturnTypeSingle()
×
185
                                returnType = append(returnType, typ)
×
186
                                if p.current().Type == ast.TokenComma {
×
187
                                        p.advance()
×
188
                                } else {
×
189
                                        break
×
190
                                }
191
                        }
192
                        p.expect(ast.TokenRParen)
×
193
                } else {
7✔
194
                        returnType = append(returnType, p.parseReturnTypeSingle())
7✔
195
                }
7✔
196
        }
197
        return returnType
34✔
198
}
199

200
func (p *Parser) parseReturnTypeSingle() ast.TypeNode {
7✔
201
        // Special handling for shape types in return positions
7✔
202
        if p.current().Type == ast.TokenLBrace {
8✔
203
                p.parseShapeType() // Parse the shape but don't use it for return type
1✔
204
                // For return types, create a simple shape type instead of an assertion
1✔
205
                return ast.TypeNode{
1✔
206
                        Ident: ast.TypeShape,
1✔
207
                }
1✔
208
        }
1✔
209
        return p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
6✔
210
}
211

212
func (p *Parser) parseReturnStatement() ast.ReturnNode {
24✔
213
        p.advance() // Move past `return`
24✔
214

24✔
215
        // Parse multiple return values
24✔
216
        values := []ast.ExpressionNode{}
24✔
217

24✔
218
        // Parse first expression
24✔
219
        if p.current().Type != ast.TokenSemicolon && p.current().Type != ast.TokenRBrace {
48✔
220
                values = append(values, p.parseExpression())
24✔
221
        }
24✔
222

223
        // Parse additional expressions separated by commas
224
        for p.current().Type == ast.TokenComma {
26✔
225
                p.advance() // Consume comma
2✔
226
                values = append(values, p.parseExpression())
2✔
227
        }
2✔
228

229
        return ast.ReturnNode{
24✔
230
                Values: values,
24✔
231
                Type:   ast.TypeNode{Ident: ast.TypeImplicit},
24✔
232
        }
24✔
233
}
234

235
func (p *Parser) parseFunctionBody() []ast.Node {
34✔
236
        return p.parseBlock()
34✔
237
}
34✔
238

239
// Parse a function definition
240
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
34✔
241
        p.expect(ast.TokenFunc)               // Expect `fn`
34✔
242
        name := p.expect(ast.TokenIdentifier) // Function name
34✔
243

34✔
244
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
34✔
245

34✔
246
        params := p.parseFunctionSignature() // Parse function parameters
34✔
247

34✔
248
        returnType := p.parseReturnType()
34✔
249

34✔
250
        body := p.parseFunctionBody()
34✔
251

34✔
252
        node := ast.FunctionNode{
34✔
253
                Ident:       ast.Ident{ID: ast.Identifier(name.Value)},
34✔
254
                ReturnTypes: returnType,
34✔
255
                Params:      params,
34✔
256
                Body:        body,
34✔
257
        }
34✔
258

34✔
259
        return node
34✔
260
}
34✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc