this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

lsp/eval: Path resolution must force eval of import specs

The LSP evaluator processes import specs super lazily so that linking
the import spec to the remote pkg evaluator is done as late as possible.
Without this, booting an evaluator would also boot every evaluator for
every imported package. And the transitive closure. So everything in the
import graph would get booted, which we don't want.

So the remote pkg evaluator only gets booted when we really pass into
it. E.g.

-- a.cue --
package a
import "b"
x: b.y
-- b.cue --
package b
y: 12

Only when we resolve the `y` of the path `b.y` do we boot pkg `b`.
But this causes a problem because it means that if you use the entire
package (e.g. changing `x: b.y` to just `x: b`) then pkg b never gets
booted and so we would also never record that pkg b is used by pkg a.

This CL fixes this by detecting when a path component resolves to an
ImportSpec and forcing the evaluation at that time. This still should
ensure that we don't fully evaluate import specs too early, but it does
ensure that the "usedBy" of packages is now correct.

The old behaviour was flakey, and for that reason it's not possible to
write a test that shows the bad behaviour. But manually backporting the
new test and running with -count 10 will show Usages being flakey.

Signed-off-by: Matthew Sackman <matthew@cue.works>
Change-Id: I5d02df1a5ccb17a9fd38deb22d0581c9f581e8c6
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1228808
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>

+93 -20
+19
internal/lsp/eval/eval.go
··· 2564 2564 components[i+1].unexpanded = unexpanded 2565 2565 if pc.node != nil { 2566 2566 for _, nav := range unexpanded { 2567 + // We eval import specs in two stages (see [frame.eval]) 2568 + // so that we delay booting remote pkg evals as long as 2569 + // possible. Because of this, if we resolve an ident to an 2570 + // import spec, we must eval it "early" otherwise use of 2571 + // the remote pkg may not be recorded correct. E.g. 2572 + // 2573 + // package a 2574 + // import "b" 2575 + // c: b 2576 + // 2577 + // When resolving the path b on line 3, if we don't eval 2578 + // the import spec here, we would never record that pkg b 2579 + // is used by pkg a. 2580 + for _, fr := range nav.frames { 2581 + if _, ok := fr.node.(*ast.ImportSpec); ok { 2582 + nav.eval() 2583 + break 2584 + } 2585 + } 2567 2586 nav.recordUsage(pc.node, p.frame) 2568 2587 } 2569 2588 }
+74 -20
internal/lsp/eval/eval_test.go
··· 3798 3798 }, 3799 3799 3800 3800 { 3801 + name: "Resolve_Import_Embed", 3802 + archive: `-- a.cue -- 3803 + package a 3804 + -- b.cue -- 3805 + package b 3806 + 3807 + import "a" 3808 + 3809 + y: z(a) 3810 + -- c.cue -- 3811 + package a 3812 + -- d.cue -- 3813 + package d 3814 + 3815 + import x "a" 3816 + import "a" 3817 + 3818 + y: a / true 3819 + `, 3820 + expectDefinitions: map[position][]position{ 3821 + fln("b.cue", 3, 1, `"a"`): {fln("a.cue", 1, 3, "a"), fln("c.cue", 1, 3, "a")}, 3822 + fln("b.cue", 5, 1, "a"): {fln("b.cue", 3, 1, `"a"`)}, 3823 + 3824 + fln("d.cue", 3, 1, "x"): {fln("a.cue", 1, 3, "a"), fln("c.cue", 1, 3, "a")}, 3825 + fln("d.cue", 4, 1, `"a"`): {fln("a.cue", 1, 3, "a"), fln("c.cue", 1, 3, "a")}, 3826 + fln("d.cue", 6, 1, "a"): {fln("d.cue", 3, 1, "x"), fln("d.cue", 4, 1, `"a"`)}, 3827 + 3828 + fln("a.cue", 1, 3, "a"): {self, fln("c.cue", 1, 3, "a")}, 3829 + fln("b.cue", 1, 1, "b"): {self}, 3830 + fln("c.cue", 1, 3, "a"): {self, fln("a.cue", 1, 3, "a")}, 3831 + fln("d.cue", 1, 1, "d"): {self}, 3832 + 3833 + fln("b.cue", 5, 1, "y"): {self}, 3834 + fln("d.cue", 6, 1, "y"): {self}, 3835 + }, 3836 + expectCompletions: map[offsetRange]fieldEmbedCompletions{ 3837 + orf("b.cue", 10, 18): {f: []string{"y"}}, 3838 + orf("b.cue", 18, 22): {e: []string{"a", "y"}}, 3839 + orf("b.cue", 22, 25): {f: []string{"y"}}, 3840 + orf("b.cue", 25, 31): {e: []string{"a", "y"}}, 3841 + 3842 + orf("d.cue", 10, 18): {f: []string{"y"}}, 3843 + orf("d.cue", 18, 24): {e: []string{"a", "x", "y"}}, 3844 + orf("d.cue", 24, 31): {f: []string{"y"}}, 3845 + orf("d.cue", 31, 35): {e: []string{"a", "x", "y"}}, 3846 + orf("d.cue", 35, 38): {f: []string{"y"}}, 3847 + orf("d.cue", 38, 43): {e: []string{"a", "x", "y"}}, 3848 + orf("d.cue", 47, 48): {e: []string{"a", "x", "y"}}, 3849 + }, 3850 + 3851 + expectUsagesExtra: map[position]map[bool][]position{ 3852 + fln("b.cue", 3, 1, `"a"`): {true: []position{self}}, 3853 + fln("d.cue", 3, 1, "x"): {true: []position{self, fln("d.cue", 4, 1, `"a"`)}}, 3854 + fln("d.cue", 4, 1, `"a"`): {true: []position{self, fln("d.cue", 3, 1, "x")}}, 3855 + }, 3856 + importedBy: map[string][]string{ 3857 + "a": {"b", "d"}, 3858 + }, 3859 + }, 3860 + 3861 + { 3801 3862 name: "Resolve_Import_Repeated", 3802 3863 archive: `-- a.cue -- 3803 3864 package a ··· 4373 4434 } 4374 4435 4375 4436 analyse := func() testCaseAnalysis { 4376 - evalByFilename := make(map[string]*eval.FileEvaluator) 4437 + evalByFilename := make(map[string]*eval.Evaluator) 4377 4438 evalByPkgName := make(map[string]*eval.Evaluator) 4378 4439 forPackage := func(importPath ast.ImportPath) *eval.Evaluator { 4379 4440 return evalByPkgName[importPath.String()] 4380 4441 } 4381 4442 importCanonicalisation := make(map[string]ast.ImportPath) 4382 4443 analysis := testCaseAnalysis{ 4383 - evalByPkgName: evalByPkgName, 4384 4444 evalByFilename: evalByFilename, 4385 4445 } 4386 4446 ··· 4398 4458 eval := eval.New(ip, importCanonicalisation, forPackage, pkgImporters, files...) 4399 4459 evalByPkgName[pkgName] = eval 4400 4460 for _, fileAst := range files { 4401 - evalByFilename[fileAst.Filename] = eval.ForFile(fileAst.Filename) 4461 + evalByFilename[fileAst.Filename] = eval 4402 4462 } 4403 4463 } 4404 4464 return analysis ··· 4414 4474 } 4415 4475 4416 4476 type testCaseAnalysis struct { 4417 - evalByPkgName map[string]*eval.Evaluator 4418 - evalByFilename map[string]*eval.FileEvaluator 4477 + evalByFilename map[string]*eval.Evaluator 4419 4478 } 4420 4479 4421 4480 func (tc *testCase) testDefinitions(t *testing.T, files []*ast.File, analysis testCaseAnalysis) { ··· 4425 4484 4426 4485 for posFrom, positionsWant := range tc.expectDefinitions { 4427 4486 filename := posFrom.filename 4428 - fileEval := evalByFilename[filename] 4487 + fileEval := evalByFilename[filename].ForFile(filename) 4429 4488 qt.Check(t, qt.IsNotNil(fileEval)) 4430 4489 4431 4490 offset := posFrom.offset ··· 4457 4516 // expectations, resolve to nothing. 4458 4517 for _, fileAst := range files { 4459 4518 filename := fileAst.Filename 4460 - fileEval := evalByFilename[filename] 4519 + fileEval := evalByFilename[filename].ForFile(filename) 4461 4520 for i := range fileAst.Pos().File().Content() { 4462 4521 if ranges.Contains(filename, i) { 4463 4522 continue ··· 4529 4588 if slices.Contains(posDfns, self) { 4530 4589 continue 4531 4590 } else if strings.HasSuffix(posUse.str, `"]`) { 4532 - // If posUse ends with "] then we assume it's a dynamic 4533 - // index into a struct. These can be inverted. E.g. 4591 + // If posUse ends with "] then we assume it's a const 4592 + // string dynamic index into a struct. These can be 4593 + // inverted. E.g. 4534 4594 // 4535 4595 // {"g": 13}["g"] 4536 4596 // ··· 4608 4668 4609 4669 declarations := make(map[position][]position) 4610 4670 for posUse, posDfns := range tc.expectDefinitions { 4611 - // Package-declarations are slightly special and so we have 4612 - // to make sure they are not treated as declarations for our 4613 - // purposes here. 4614 - if posUse.offset == 8 && strings.HasPrefix(posUse.content, "package ") { 4615 - continue 4616 - } 4617 4671 posDfns := slices.Clone(posDfns) 4618 4672 for i, posDfn := range posDfns { 4619 4673 if posDfn == self { // this must be a field declaration ··· 4684 4738 4685 4739 for posUse, positionsWant := range expectUsages { 4686 4740 filename := posUse.filename 4687 - fe := analysis.evalByFilename[filename] 4741 + fe := analysis.evalByFilename[filename].ForFile(filename) 4688 4742 qt.Assert(t, qt.IsNotNil(fe)) 4689 4743 4690 4744 fileOffsetsWant := make([]fileOffset, len(positionsWant)) ··· 4727 4781 slices.Sort(fieldCompletionWant) 4728 4782 slices.Sort(embedCompletionWant) 4729 4783 filename := curRange.filename 4730 - fe := evalByFilename[filename] 4784 + fe := evalByFilename[filename].ForFile(filename) 4731 4785 qt.Check(t, qt.IsNotNil(fe)) 4732 4786 4733 4787 ranges.Add(filename, curRange.from, curRange.to) ··· 4756 4810 // expectations, complete to nothing. 4757 4811 for _, fileAst := range files { 4758 4812 filename := fileAst.Filename 4759 - fe := evalByFilename[filename] 4813 + fe := evalByFilename[filename].ForFile(filename) 4760 4814 4761 4815 for i := range fileAst.Pos().File().Content() { 4762 4816 if ranges.Contains(filename, i) { ··· 4769 4823 }) 4770 4824 } 4771 4825 4772 - func (tc *testCase) dumpCompletions(t *testing.T, files []*ast.File, evalByFilename map[string]*eval.FileEvaluator) { 4826 + func (tc *testCase) dumpCompletions(t *testing.T, files []*ast.File, evalByFilename map[string]*eval.Evaluator) { 4773 4827 for _, fileAst := range files { 4774 4828 filename := fileAst.Filename 4775 - fe := evalByFilename[filename] 4829 + fe := evalByFilename[filename].ForFile(filename) 4776 4830 content := fileAst.Pos().File().Content() 4777 4831 4778 4832 var strs []string