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

chore: migrate verb schema parsing #1711

Merged
merged 1 commit into from
Jun 17, 2024

Conversation

worstell
Copy link
Contributor

@worstell worstell commented Jun 8, 2024

restructuring and adding some additional passes to provide data for the extractors-

  1. metadata pass parses all comments in the packages and provides results via facts
  2. transitive pass extracts nodes marked as needing extraction via implicit reference from other decls

#1518

@worstell worstell requested a review from alecthomas as a code owner June 8, 2024 00:17
@worstell worstell requested review from a team and deniseli and removed request for a team June 8, 2024 00:17
@ftl-robot ftl-robot mentioned this pull request Jun 8, 2024
@worstell worstell force-pushed the worstell/20240607-migrate-verb-schema-parsing branch 2 times, most recently from 3333831 to 4a06dff Compare June 8, 2024 00:21
Copy link
Contributor

@deniseli deniseli left a comment

Choose a reason for hiding this comment

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

This is awesome!!! Thanks so much for doing this

"golang.org/x/exp/maps"
)

var (
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make these const now?


// Initializer prepares data prior to the schema extractor runs, e.g. loads FTL types for reference by other
// analyzers.
var Initializer = &analysis.Analyzer{
Copy link
Contributor

Choose a reason for hiding this comment

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

All of this is super nice :D

ftlTypes map[string]*types.Interface
}

func (r initializeResult) isFtlErrorType(typ types.Type) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

nittiest of nits: isFTLErrorType for go style

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm also not sure what these functions mean, can you add some comments? What is an "ftl error type", for example?

return r.assertableToFtlType(typ, "builtin", "error")
}

func (r initializeResult) isFtlContextType(typ types.Type) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

should this just be isContextType since we don't have our own special context type and just use the standard Go one?

return nil, err
}
if len(pkgs) != 1 {
return nil, errors.New("expected one package")
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we make this expected one package, got %d? Since that usually saves some time debugging whenever an error like that does appear

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 and maybe even do something like strings.Join(slices.Map(pkgs, func(p *packages.Package) (string, error) { return p.Name }), ", ")

var VerbExtractor = newExtractor("verb", extractVerbs, (*verbExtractorFact)(nil))

type verbExtractorFact struct {
decl schema.Decl
Copy link
Contributor

Choose a reason for hiding this comment

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

would this always be a schema.Verb? Or do we need to leave this as a schema.Decl to make the interface happy?

if results.Len() == 0 {
pass.Report(errorf(node, "must at least return an error"))
} else if !loaded.isFtlErrorType(results.At(results.Len() - 1).Type()) {
pass.Report(tokenErrorf(pass.Fset, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type()))
Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, when returning multiple values, should we specify in the error message that last value in the return statement must be error, in case they have something like func(context.Context) (error, struct)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that's what this is already doing isn't it? results.At(results.Len()-1) is the last out value.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I just meant clarifying the error message. The logic itself looks good yeah

@@ -21,7 +21,8 @@ func Extract(moduleDir string) (analyzers.ExtractResult, error) {
RunDespiteLoadErrors: true,
Patterns: []string{"./..."},
}
results, diagnostics, err := checker.Run(cConfig, append(analyzers.Extractors, analyzers.Finalizer)...)
as := []*analysis.Analyzer{analyzers.Initializer, analyzers.Finalizer}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not related to this line, just a general comment: did you happen to check if we need to add any more test coverage? I can't recall if we're missing any significant branch cases for the new approach.

Copy link
Collaborator

@alecthomas alecthomas left a comment

Choose a reason for hiding this comment

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

So good!

ResultType: reflect.TypeFor[ExtractResult](),
RunDespiteErrors: true,
}

// ExtractResult contains the final schema extraction result.
// ExtractResult contains the final schema extraction extractorResult.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: overly enthusiastic type rename

ftlTypes map[string]*types.Interface
}

func (r initializeResult) isFtlErrorType(typ types.Type) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm also not sure what these functions mean, can you add some comments? What is an "ftl error type", for example?

return nil, err
}
if len(pkgs) != 1 {
return nil, errors.New("expected one package")
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 and maybe even do something like strings.Join(slices.Map(pkgs, func(p *packages.Package) (string, error) { return p.Name }), ", ")

}
obj := pkgs[0].Types.Scope().Lookup(name)
if obj == nil {
return nil, errors.New("interface not found")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return nil, errors.New("interface not found")
return nil, errors.Errorf("interface %q not found", name)

}
ifaceType, ok := obj.Type().Underlying().(*types.Interface)
if !ok {
return nil, errors.New("expected an interface, got " + obj.Type().String())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return nil, errors.New("expected an interface, got " + obj.Type().String())
return nil, errors.Errorf("expected an interface, got %s" + obj.Type())

nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
in.Preorder(nodeFilter, func(n ast.Node) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh in.Preorder() is super handy, nice nice. We can delete my shitty astwalk function at some point.

reqV, reqOk := req.Get()
resV, respOk := resp.Get()
if !reqOk {
pass.Report(tokenErrorf(pass.Fset, params.At(1).Pos(), params.At(1).Name(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Noice.

return newExtractorResult(pass, verbs, nn), nil
}

func extractVerbMetadata(pass *analysis.Pass, node ast.Node, directives []directive) []schema.Metadata {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Metadata extraction could be another pass. We could look for any //ftl: directives on any declaration, and add them as facts to the node. Then in verb analysis, pubsub, types, etc. we just pull the metadata fact directly from the node.

if results.Len() == 0 {
pass.Report(errorf(node, "must at least return an error"))
} else if !loaded.isFtlErrorType(results.At(results.Len() - 1).Type()) {
pass.Report(tokenErrorf(pass.Fset, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that's what this is already doing isn't it? results.At(results.Len()-1) is the last out value.

}

if results.Len() > 2 {
pass.Report(errorf(node, "must have at most two results (struct, error)"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
pass.Report(errorf(node, "must have at most two results (struct, error)"))
pass.Report(errorf(node, "must have at most two results (<type>, error)"))

@worstell worstell force-pushed the worstell/20240607-migrate-verb-schema-parsing branch 17 times, most recently from b084e25 to 07db4f9 Compare June 17, 2024 20:06
restructuring and adding some additional passes to provide data for the extractors-
1. metadata pass parses all comments in the packages and provides results via facts
2. transitive pass extracts nodes marked as needing extraction via transitive reference from other decls
@worstell worstell force-pushed the worstell/20240607-migrate-verb-schema-parsing branch from 07db4f9 to e0e5a12 Compare June 17, 2024 20:09
@worstell
Copy link
Contributor Author

ok, completely overhauled this change 😅 the scope creep required to get something working was kind of insane. it's quite different now but all comments should be addressed!

this pr has gotten kind of out of hand but the next ones should be muuuuuch better contained for migrating future pieces.

@worstell worstell merged commit 15b653f into main Jun 17, 2024
41 checks passed
@worstell worstell deleted the worstell/20240607-migrate-verb-schema-parsing branch June 17, 2024 20:17
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.

3 participants