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

forst-lang / forst / 16089939345

05 Jul 2025 04:20PM UTC coverage: 39.652% (+2.3%) from 37.308%
16089939345

push

github

web-flow
feat: Shape guards (#17)

2211 of 6179 new or added lines in 87 files covered. (35.78%)

102 existing lines in 32 files now uncovered.

3719 of 9379 relevant lines covered (39.65%)

6.47 hits per line

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

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

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

UNCOV
7
func (p *Parser) parseParameterType() ast.TypeNode {
×
UNCOV
8
        next := p.peek()
×
UNCOV
9
        // TODO: Even if the next token is a dot this could be a type without any constraints
×
UNCOV
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
NEW
18
        if p.current().Type == ast.TokenIdentifier && p.current().Value == "Shape" {
×
NEW
19
                ident := p.expect(ast.TokenIdentifier)
×
NEW
20
                if p.current().Type == ast.TokenLParen {
×
NEW
21
                        p.FailWithParseError(p.current(), "Shape({...}) wrapper is not allowed. Use {...} directly for shape types.")
×
NEW
22
                }
×
NEW
23
                return ast.TypeNode{
×
NEW
24
                        Ident: ast.TypeIdent(ident.Value),
×
NEW
25
                }
×
26
        }
27
        // Allow direct {...} for shape types
NEW
28
        if p.current().Type == ast.TokenLBrace {
×
NEW
29
                shape := p.parseShape(nil)
×
NEW
30
                baseType := ast.TypeIdent(ast.TypeShape)
×
NEW
31
                return ast.TypeNode{
×
NEW
32
                        Ident: ast.TypeShape,
×
NEW
33
                        Assertion: &ast.AssertionNode{
×
NEW
34
                                BaseType: &baseType,
×
NEW
35
                                Constraints: []ast.ConstraintNode{{
×
NEW
36
                                        Name: "Match",
×
NEW
37
                                        Args: []ast.ConstraintArgumentNode{{
×
NEW
38
                                                Shape: &shape,
×
NEW
39
                                        }},
×
NEW
40
                                }},
×
NEW
41
                        },
×
NEW
42
                }
×
NEW
43
        }
×
UNCOV
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 {
4✔
74
        name := p.expect(ast.TokenIdentifier).Value
4✔
75
        // Remove colon requirement - use Go-style parameter declarations
4✔
76

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

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

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

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

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

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

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

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

176
func (p *Parser) parseReturnType() []ast.TypeNode {
31✔
177
        returnType := []ast.TypeNode{}
31✔
178
        if p.current().Type == ast.TokenColon {
38✔
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✔
NEW
182
                        p.advance() // Consume '('
×
NEW
183
                        for {
×
NEW
184
                                typ := p.parseType(TypeIdentOpts{AllowLowercaseTypes: false})
×
NEW
185
                                returnType = append(returnType, typ)
×
NEW
186
                                if p.current().Type == ast.TokenComma {
×
NEW
187
                                        p.advance()
×
NEW
188
                                } else {
×
NEW
189
                                        break
×
190
                                }
191
                        }
NEW
192
                        p.expect(ast.TokenRParen)
×
193
                } else {
7✔
194
                        returnType = append(returnType, p.parseType(TypeIdentOpts{AllowLowercaseTypes: false}))
7✔
195
                }
7✔
196
        }
197
        return returnType
31✔
198
}
199

200
func (p *Parser) parseReturnStatement() ast.ReturnNode {
22✔
201
        p.advance() // Move past `return`
22✔
202

22✔
203
        // Parse multiple return values
22✔
204
        values := []ast.ExpressionNode{}
22✔
205

22✔
206
        // Parse first expression
22✔
207
        if p.current().Type != ast.TokenSemicolon && p.current().Type != ast.TokenRBrace {
44✔
208
                values = append(values, p.parseExpression())
22✔
209
        }
22✔
210

211
        // Parse additional expressions separated by commas
212
        for p.current().Type == ast.TokenComma {
23✔
213
                p.advance() // Consume comma
1✔
214
                values = append(values, p.parseExpression())
1✔
215
        }
1✔
216

217
        return ast.ReturnNode{
22✔
218
                Values: values,
22✔
219
                Type:   ast.TypeNode{Ident: ast.TypeImplicit},
22✔
220
        }
22✔
221
}
222

223
func (p *Parser) parseFunctionBody() []ast.Node {
31✔
224
        return p.parseBlock()
31✔
225
}
31✔
226

227
// Parse a function definition
228
func (p *Parser) parseFunctionDefinition() ast.FunctionNode {
31✔
229
        p.expect(ast.TokenFunc)               // Expect `fn`
31✔
230
        name := p.expect(ast.TokenIdentifier) // Function name
31✔
231

31✔
232
        p.context.ScopeStack.CurrentScope().FunctionName = name.Value
31✔
233

31✔
234
        params := p.parseFunctionSignature() // Parse function parameters
31✔
235

31✔
236
        returnType := p.parseReturnType()
31✔
237

31✔
238
        body := p.parseFunctionBody()
31✔
239

31✔
240
        node := ast.FunctionNode{
31✔
241
                Ident:       ast.Ident{ID: ast.Identifier(name.Value)},
31✔
242
                ReturnTypes: returnType,
31✔
243
                Params:      params,
31✔
244
                Body:        body,
31✔
245
        }
31✔
246

31✔
247
        return node
31✔
248
}
31✔
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