ttsc 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +17 -6
- package/cmd/ttscgraph/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +253 -25
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +57 -8
- package/driver/plugins.go +125 -14
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -2
- package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
- package/lib/launcher/internal/runtimeHooks.js +93 -2
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
- package/package.json +8 -8
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/shim.go +3 -0
- package/shim/compiler/emithook.go +10 -0
- package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
- package/src/index.ts +1 -0
- package/src/launcher/internal/runtimeHooks.ts +111 -2
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/structures/ITtscCompilerTransformation.ts +7 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +15 -6
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"reflect"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestBuildReleasesTheScratchItNoLongerReads verifies that a returned Graph
|
|
12
|
+
// carries only what a consumer reads, and still carries what the shard path
|
|
13
|
+
// reads after the call.
|
|
14
|
+
//
|
|
15
|
+
// Every field asserted nil below is documented build-only and holds pointers
|
|
16
|
+
// into the compiler AST or into the preceding generation's nodes. They used to
|
|
17
|
+
// survive the build, so a consumer retaining the Graph pinned all of it —
|
|
18
|
+
// internal/graphsymbols keeps one for the lifetime of an editor session between
|
|
19
|
+
// invalidations, closing the Program while the maps referencing its AST live on.
|
|
20
|
+
// Measured on this repository's own packages, holding the Graph after closing
|
|
21
|
+
// the Program retained 38.1 MB for @ttsc/lint, 52.5 MB for ttsc, and 58.2 MB for
|
|
22
|
+
// @ttsc/graph; releasing the scratch brings those to 3.7, 5.0, and 4.0 MB.
|
|
23
|
+
//
|
|
24
|
+
// The assertion is structural rather than a heap measurement on purpose: a
|
|
25
|
+
// megabyte threshold is a flaky test, while "the producer stopped holding it" is
|
|
26
|
+
// the invariant and is exact.
|
|
27
|
+
//
|
|
28
|
+
// 1. Build the complete graph for a one-file project.
|
|
29
|
+
// 2. Assert every build-only field is released.
|
|
30
|
+
// 3. Assert the two fields the shard expansion reads after the call, and the
|
|
31
|
+
// graph itself, are not.
|
|
32
|
+
func TestBuildReleasesTheScratchItNoLongerReads(t *testing.T) {
|
|
33
|
+
root := t.TempDir()
|
|
34
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), `{
|
|
35
|
+
"compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
|
|
36
|
+
"files": ["src/main.ts"]
|
|
37
|
+
}
|
|
38
|
+
`)
|
|
39
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/a.md#x Cited. */
|
|
40
|
+
export interface ISale { id: string }
|
|
41
|
+
export class Store implements ISale {
|
|
42
|
+
public id: string = "";
|
|
43
|
+
public save(): void { this.load() }
|
|
44
|
+
public load(): void {}
|
|
45
|
+
}
|
|
46
|
+
export function run(store: Store): void { store.save() }
|
|
47
|
+
`)
|
|
48
|
+
|
|
49
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
50
|
+
if err != nil || prog == nil {
|
|
51
|
+
t.Fatalf("could not load the probe project: %v", err)
|
|
52
|
+
}
|
|
53
|
+
defer func() { _ = prog.Close() }()
|
|
54
|
+
|
|
55
|
+
g := Build(prog)
|
|
56
|
+
assertScratchReleased(t, g, "complete build")
|
|
57
|
+
|
|
58
|
+
// The partial build is not a second case of the same thing: `baseNodes` and
|
|
59
|
+
// `selectedFiles` are nil on arrival in a complete build, so a complete build
|
|
60
|
+
// alone cannot fail on them. `baseNodes` is the largest of the nine and the
|
|
61
|
+
// one #1243 flagged as the caution, because it is passed in rather than built.
|
|
62
|
+
var mainFile string
|
|
63
|
+
for _, node := range g.Nodes {
|
|
64
|
+
mainFile = node.File
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
if mainFile == "" {
|
|
68
|
+
t.Fatal("the probe project produced no node to select a file from")
|
|
69
|
+
}
|
|
70
|
+
partial := BuildFiles(prog, []string{mainFile}, g.Nodes)
|
|
71
|
+
assertScratchReleased(t, partial, "partial build")
|
|
72
|
+
|
|
73
|
+
// The negative twin. Clearing scratch must not clear what the shard expansion
|
|
74
|
+
// in cmd/ttscgraph/serve_shards.go reads after BuildFiles returns.
|
|
75
|
+
if g.ExportedTargets == nil {
|
|
76
|
+
t.Error("ExportedTargets was released, but the shard path reads it after the build")
|
|
77
|
+
}
|
|
78
|
+
if g.ImplementationSources == nil {
|
|
79
|
+
t.Error("ImplementationSources was released, but the shard path reads it after the build")
|
|
80
|
+
}
|
|
81
|
+
if len(g.Nodes) == 0 || len(g.Edges) == 0 || len(g.DocTags) == 0 {
|
|
82
|
+
t.Fatalf("the graph itself is empty: %d nodes, %d edges, %d tags",
|
|
83
|
+
len(g.Nodes), len(g.Edges), len(g.DocTags))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// assertScratchReleased requires every unexported reference field of a returned
|
|
88
|
+
// Graph to be nil.
|
|
89
|
+
//
|
|
90
|
+
// It reflects rather than naming the fields, because a hand-written list has to
|
|
91
|
+
// be edited by the same person who forgets to edit releaseBuildState — which is
|
|
92
|
+
// exactly how docHosts arrived beside resolved. Unexported fields are readable
|
|
93
|
+
// this way: reflect.Value.IsNil needs no exported access, only Interface does.
|
|
94
|
+
//
|
|
95
|
+
// Exported fields are skipped: Nodes, Edges, Decorators, and DocTags are the
|
|
96
|
+
// graph, and ExportedTargets and ImplementationSources are read after the build.
|
|
97
|
+
func assertScratchReleased(t *testing.T, g *Graph, label string) {
|
|
98
|
+
t.Helper()
|
|
99
|
+
value := reflect.ValueOf(g).Elem()
|
|
100
|
+
for index := 0; index < value.NumField(); index++ {
|
|
101
|
+
field := value.Type().Field(index)
|
|
102
|
+
if field.IsExported() {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
switch value.Field(index).Kind() {
|
|
106
|
+
case reflect.Map, reflect.Slice, reflect.Ptr:
|
|
107
|
+
default:
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if !value.Field(index).IsNil() {
|
|
111
|
+
t.Errorf(
|
|
112
|
+
"%s: %s survived; it pins the AST for as long as a consumer holds the graph",
|
|
113
|
+
label,
|
|
114
|
+
field.Name,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// unusedLocalsFixtureTSConfig is the shared fixture config with the one flag
|
|
12
|
+
// this test is about. `noUnusedLocals` is what makes the compiler state, in a
|
|
13
|
+
// diagnostic, whether it counted a documentation link as a use.
|
|
14
|
+
const unusedLocalsFixtureTSConfig = `{
|
|
15
|
+
"compilerOptions": {
|
|
16
|
+
"target": "ES2022",
|
|
17
|
+
"module": "commonjs",
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"rootDir": "src",
|
|
21
|
+
"outDir": "dist"
|
|
22
|
+
},
|
|
23
|
+
"files": ["src/main.ts"]
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
// TestDocRefsAreTheUseTheCheckerAlreadyCounts verifies the premise this edge
|
|
28
|
+
// rests on: the compiler resolves a documentation link and counts it as a use,
|
|
29
|
+
// and recording the edge does not change what it reports.
|
|
30
|
+
//
|
|
31
|
+
// The whole argument for the edge is that it is a compiler fact rather than a
|
|
32
|
+
// text match, and the compiler says so itself — an import supporting only a link
|
|
33
|
+
// survives `noUnusedLocals` while the same import without one does not. That
|
|
34
|
+
// asymmetry is the evidence the issue was opened on, and it lived only in its
|
|
35
|
+
// prose. Pinning it here matters twice: it fails if a TypeScript upgrade ever
|
|
36
|
+
// stops counting links as uses, which would make the edge a text match dressed
|
|
37
|
+
// as a checker fact, and it fails if this pass ever changes what the compiler
|
|
38
|
+
// reports, which it must never do.
|
|
39
|
+
//
|
|
40
|
+
// 1. Build one file importing two types, using one only through a link and the
|
|
41
|
+
// other not at all.
|
|
42
|
+
// 2. Assert the compiler reports the unlinked import unused and the linked one
|
|
43
|
+
// not.
|
|
44
|
+
// 3. Assert the link produced its edge in the same build.
|
|
45
|
+
func TestDocRefsAreTheUseTheCheckerAlreadyCounts(t *testing.T) {
|
|
46
|
+
root := t.TempDir()
|
|
47
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), unusedLocalsFixtureTSConfig)
|
|
48
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `interface ILinked {
|
|
49
|
+
a: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface IUnlinked {
|
|
53
|
+
b: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Names {@link ILinked} and nothing else. */
|
|
57
|
+
export function subject(): void {}
|
|
58
|
+
`)
|
|
59
|
+
|
|
60
|
+
prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
61
|
+
if err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
defer func() { _ = prog.Close() }()
|
|
65
|
+
|
|
66
|
+
linked, unlinked := false, false
|
|
67
|
+
for _, diagnostic := range prog.Diagnostics() {
|
|
68
|
+
message := diagnostic.String()
|
|
69
|
+
if !strings.Contains(message, "never used") &&
|
|
70
|
+
!strings.Contains(message, "declared but") {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if strings.Contains(message, "ILinked") {
|
|
74
|
+
linked = true
|
|
75
|
+
}
|
|
76
|
+
if strings.Contains(message, "IUnlinked") {
|
|
77
|
+
unlinked = true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if !unlinked {
|
|
81
|
+
t.Fatal("the unlinked declaration was not reported unused, so this fixture " +
|
|
82
|
+
"cannot demonstrate the asymmetry the edge rests on")
|
|
83
|
+
}
|
|
84
|
+
if linked {
|
|
85
|
+
t.Fatal("the linked declaration was reported unused, so the compiler no " +
|
|
86
|
+
"longer counts a documentation link as a use and this edge is a text " +
|
|
87
|
+
"match rather than a checker fact")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The same build produced the edge, so the fact the compiler counted and the
|
|
91
|
+
// fact the graph records are one fact.
|
|
92
|
+
assertDocRef(t, Build(prog), "#subject:function", "#ILinked:interface")
|
|
93
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestDocRefsResolveDocumentationLinks verifies that an inline link in a
|
|
12
|
+
// declaration's documentation becomes a checker-resolved edge, and that the tag
|
|
13
|
+
// it sits under changes nothing.
|
|
14
|
+
//
|
|
15
|
+
// The checker already resolves such a name and counts it as a use — the
|
|
16
|
+
// companion negative case is a project where `noUnusedLocals` keeps an import
|
|
17
|
+
// that only a link supports — so this was the one class of resolved reference
|
|
18
|
+
// the graph held no edge for, and the citation-only `import type` a citation
|
|
19
|
+
// convention recommends is exactly the form nothing else in the module records.
|
|
20
|
+
//
|
|
21
|
+
// 1. Build a fixture linking one type from a tag, one from ordinary prose, one
|
|
22
|
+
// through `{@linkcode}`, one qualified, and one that resolves to nothing.
|
|
23
|
+
// 2. Assert each resolvable link produced exactly one doc_ref edge to the
|
|
24
|
+
// declaration the checker resolved.
|
|
25
|
+
// 3. Assert the unresolvable link, the self-link, and the untagged declaration
|
|
26
|
+
// produced none.
|
|
27
|
+
// 4. Declare the same name in two more modules that flank it alphabetically,
|
|
28
|
+
// import all three, and assert every link resolves to the one this module
|
|
29
|
+
// imports the name from — the claim the rest of the file now rests on,
|
|
30
|
+
// since three declarations of `ISale` exist here.
|
|
31
|
+
func TestDocRefsResolveDocumentationLinks(t *testing.T) {
|
|
32
|
+
root := t.TempDir()
|
|
33
|
+
writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
|
|
34
|
+
// Two rivals, one sorting before `sale` and one after, each imported so the
|
|
35
|
+
// program holds it. One rival is not enough: a text matcher has to break the
|
|
36
|
+
// tie somehow, and whichever end it picks — lowest id, highest id, first or
|
|
37
|
+
// last in program order — a single rival leaves half of those choices landing
|
|
38
|
+
// on the right answer by luck. Flanking the real module closes every one.
|
|
39
|
+
writeFile(t, filepath.Join(root, "src", "archive.ts"), `export interface ISale {
|
|
40
|
+
archived: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface IArchive {
|
|
44
|
+
flag: boolean;
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
writeFile(t, filepath.Join(root, "src", "vendor.ts"), `export interface ISale {
|
|
48
|
+
vendored: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface IVendor {
|
|
52
|
+
flag: boolean;
|
|
53
|
+
}
|
|
54
|
+
`)
|
|
55
|
+
writeFile(t, filepath.Join(root, "src", "sale.ts"), `export interface ISale {
|
|
56
|
+
price: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export namespace Shopping {
|
|
60
|
+
export interface ICoupon {
|
|
61
|
+
code: string;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`)
|
|
65
|
+
writeFile(t, filepath.Join(root, "src", "main.ts"), `// The real module is imported between its two rivals, so no end of the import
|
|
66
|
+
// order is the right answer either.
|
|
67
|
+
import type { IArchive } from "./archive";
|
|
68
|
+
import type { ISale, Shopping } from "./sale";
|
|
69
|
+
import type { IVendor } from "./vendor";
|
|
70
|
+
|
|
71
|
+
/** @evidence {@link ISale} Cited from a tag. */
|
|
72
|
+
export function fromTag(): void {}
|
|
73
|
+
|
|
74
|
+
/** Ordinary prose naming {@link ISale} with no tag at all. */
|
|
75
|
+
export function fromProse(): void {}
|
|
76
|
+
|
|
77
|
+
/** @evidence {@linkcode ISale} Code form. */
|
|
78
|
+
export function fromCode(): void {}
|
|
79
|
+
|
|
80
|
+
/** @evidence {@linkplain ISale} Plain form. */
|
|
81
|
+
export function fromPlain(): void {}
|
|
82
|
+
|
|
83
|
+
/** Two links, {@link ISale} and {@link Shopping.ICoupon}, plus {@link ISale} twice. */
|
|
84
|
+
export function severalLinks(): void {}
|
|
85
|
+
|
|
86
|
+
/** @evidence {@link Shopping.ICoupon} Qualified. */
|
|
87
|
+
export function fromQualified(): void {}
|
|
88
|
+
|
|
89
|
+
/** @evidence {@link NoSuchSymbol} Resolves to nothing. */
|
|
90
|
+
export function fromUnresolved(): void {}
|
|
91
|
+
|
|
92
|
+
/** @see {@link ISale} A tag TypeScript does know. */
|
|
93
|
+
export function fromSee(): void {}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param value The parameter, whose tag carries no link.
|
|
97
|
+
* @returns Nothing.
|
|
98
|
+
*/
|
|
99
|
+
export function withKnownTags(value: string): void {}
|
|
100
|
+
|
|
101
|
+
/** Names {@link selfLinked} itself. */
|
|
102
|
+
export function selfLinked(): void {}
|
|
103
|
+
|
|
104
|
+
/** Carries documentation but no link. */
|
|
105
|
+
export function noLink(): void {}
|
|
106
|
+
|
|
107
|
+
// A link in a line comment: {@link ISale} is not documentation.
|
|
108
|
+
export function lineCommentLink(): void {}
|
|
109
|
+
|
|
110
|
+
export namespace Reached {
|
|
111
|
+
/** A namespace member naming {@link ISale}. */
|
|
112
|
+
export function member(): void {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function hostsClosure(): void {
|
|
116
|
+
/** A closure naming {@link ISale}. */
|
|
117
|
+
function inner(): void {}
|
|
118
|
+
inner();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Names {@link ISale}, of which this file imports exactly one. */
|
|
122
|
+
export function ambiguousName(): void {}
|
|
123
|
+
|
|
124
|
+
/** Names {@link IArchive}, which only the first rival declares. */
|
|
125
|
+
export function usesArchive(): void {}
|
|
126
|
+
|
|
127
|
+
/** Names {@link IVendor}, which only the second rival declares. */
|
|
128
|
+
export function usesVendor(): void {}
|
|
129
|
+
|
|
130
|
+
/** Names a library type: {@link Promise}. */
|
|
131
|
+
export function linksExternal(): void {}
|
|
132
|
+
|
|
133
|
+
/** A class documented with {@link ISale} on the class itself. */
|
|
134
|
+
export class Documented {
|
|
135
|
+
/** @evidence {@link Shopping.ICoupon} On the member, not the class. */
|
|
136
|
+
public readonly coupon: string = "";
|
|
137
|
+
|
|
138
|
+
/** Method documentation naming {@link ISale}. */
|
|
139
|
+
public run(): void {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** An interface documented with {@link ISale}. */
|
|
143
|
+
export interface IDocumented {
|
|
144
|
+
value: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A namespace documented with {@link ISale}. */
|
|
148
|
+
export namespace Documented2 {
|
|
149
|
+
export const value = 1;
|
|
150
|
+
}
|
|
151
|
+
`)
|
|
152
|
+
|
|
153
|
+
prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
|
|
154
|
+
if err != nil {
|
|
155
|
+
t.Fatal(err)
|
|
156
|
+
}
|
|
157
|
+
if len(diags) != 0 {
|
|
158
|
+
t.Fatalf("unexpected diagnostics: %v", diags)
|
|
159
|
+
}
|
|
160
|
+
defer func() { _ = prog.Close() }()
|
|
161
|
+
|
|
162
|
+
g := Build(prog)
|
|
163
|
+
|
|
164
|
+
assertDocRef(t, g, "#fromTag:function", "sale.ts#ISale:interface")
|
|
165
|
+
// The tag decides nothing: a link in ordinary prose is the same relation.
|
|
166
|
+
assertDocRef(t, g, "#fromProse:function", "sale.ts#ISale:interface")
|
|
167
|
+
assertDocRef(t, g, "#fromCode:function", "sale.ts#ISale:interface")
|
|
168
|
+
assertDocRef(t, g, "#fromPlain:function", "sale.ts#ISale:interface")
|
|
169
|
+
// Distinct targets each get an edge; a target named twice collapses under the
|
|
170
|
+
// uniqueness rule every edge kind shares, keeping the first span.
|
|
171
|
+
assertDocRef(t, g, "#severalLinks:function", "sale.ts#ISale:interface")
|
|
172
|
+
assertDocRef(t, g, "#severalLinks:function", "#Shopping.ICoupon:interface")
|
|
173
|
+
assertDocRef(t, g, "#fromQualified:function", "#Shopping.ICoupon:interface")
|
|
174
|
+
// A link under a tag TypeScript recognizes is the same relation. Reading a
|
|
175
|
+
// tag's comment through its per-kind struct crashed on the first `@param`
|
|
176
|
+
// instead, so this case and the one below are one fix and one regression.
|
|
177
|
+
assertDocRef(t, g, "#fromSee:function", "sale.ts#ISale:interface")
|
|
178
|
+
|
|
179
|
+
// A name the checker cannot resolve is not a relation, and must not
|
|
180
|
+
// fabricate a node to point at.
|
|
181
|
+
assertNoDocRef(t, g, "#fromUnresolved:function")
|
|
182
|
+
// A declaration naming itself is not an edge, the same rule typeRefEdge keeps.
|
|
183
|
+
assertNoDocRef(t, g, "#selfLinked:function")
|
|
184
|
+
assertNoDocRef(t, g, "#noLink:function")
|
|
185
|
+
assertNoDocRef(t, g, "#withKnownTags:function")
|
|
186
|
+
// A `//` comment is not documentation, so the parser attaches it to nothing
|
|
187
|
+
// and no link can be read out of it.
|
|
188
|
+
assertNoDocRef(t, g, "#lineCommentLink:function")
|
|
189
|
+
|
|
190
|
+
// Both nested forms are nodes the build pass records, so both resolve their
|
|
191
|
+
// own links — the edge pass reads the same host set the node pass filled.
|
|
192
|
+
assertDocRef(t, g, "#Reached.member:function", "sale.ts#ISale:interface")
|
|
193
|
+
assertDocRef(t, g, "#hostsClosure.inner:function", "sale.ts#ISale:interface")
|
|
194
|
+
|
|
195
|
+
// A class, an interface, and a namespace carry documentation of their own,
|
|
196
|
+
// and the container walk the edge pass would naturally reuse never visits any
|
|
197
|
+
// of them as a node: it descends straight into their members. Each of these
|
|
198
|
+
// resolved to nothing while the tag beside it was indexed, so the two halves
|
|
199
|
+
// a reader composes disagreed exactly where a type is documented.
|
|
200
|
+
assertDocRef(t, g, "#Documented:class", "sale.ts#ISale:interface")
|
|
201
|
+
assertDocRef(t, g, "#IDocumented:interface", "sale.ts#ISale:interface")
|
|
202
|
+
// A namespace is a grouping container the graph models as no node at all, so
|
|
203
|
+
// its own documentation has nothing to hang on — and the tag half agrees,
|
|
204
|
+
// because `putDeclaredNode` is never called for one either. The two halves
|
|
205
|
+
// being absent together is the invariant; one of them answering alone is the
|
|
206
|
+
// defect this pairing exists to catch.
|
|
207
|
+
assertNoNamespaceNode(t, g, "#Documented2:")
|
|
208
|
+
assertNoDocRefTo(t, g, "#Documented2.value:variable", "sale.ts#ISale:interface")
|
|
209
|
+
|
|
210
|
+
// A member's link belongs to the member. The same walk hands a property's
|
|
211
|
+
// subtree to its class as well, which is right for dependency edges and wrong
|
|
212
|
+
// here: the class's own documentation names nothing.
|
|
213
|
+
assertDocRef(t, g, "#Documented.coupon:variable", "#Shopping.ICoupon:interface")
|
|
214
|
+
assertNoDocRefTo(t, g, "#Documented:class", "#Shopping.ICoupon:interface")
|
|
215
|
+
assertDocRef(t, g, "#Documented.run:method", "sale.ts#ISale:interface")
|
|
216
|
+
|
|
217
|
+
// Two modules declare `ISale`, and only the checker knows which one a link
|
|
218
|
+
// means: it resolves the name through this module's imports. A text match on
|
|
219
|
+
// the written token could not tell them apart, and would have to guess.
|
|
220
|
+
assertNodeExists(t, g, "archive.ts#ISale:interface")
|
|
221
|
+
assertNodeExists(t, g, "vendor.ts#ISale:interface")
|
|
222
|
+
// Each rival is reached for a name only it declares, which is what keeps its
|
|
223
|
+
// import from being the kind a stricter project would remove.
|
|
224
|
+
assertDocRef(t, g, "#usesArchive:function", "archive.ts#IArchive:interface")
|
|
225
|
+
assertDocRef(t, g, "#usesVendor:function", "vendor.ts#IVendor:interface")
|
|
226
|
+
assertDocRef(t, g, "#ambiguousName:function", "sale.ts#ISale:interface")
|
|
227
|
+
assertNoDocRefTo(t, g, "#ambiguousName:function", "archive.ts#ISale:interface")
|
|
228
|
+
assertNoDocRefTo(t, g, "#ambiguousName:function", "vendor.ts#ISale:interface")
|
|
229
|
+
|
|
230
|
+
// A link to a library type follows the external-boundary policy every other
|
|
231
|
+
// edge kind keeps: the target is a named endpoint, not walked into.
|
|
232
|
+
assertExternalDocRef(t, g, "#linksExternal:function", "Promise")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// assertExternalDocRef fails unless exactly one doc_ref edge leaves the node and
|
|
236
|
+
// lands on an external declaration of this name.
|
|
237
|
+
func assertExternalDocRef(t *testing.T, g *Graph, fromSuffix, name string) {
|
|
238
|
+
t.Helper()
|
|
239
|
+
found := 0
|
|
240
|
+
for _, edge := range g.Edges {
|
|
241
|
+
if edge.Kind != EdgeDocRef || !suffixMatch(edge.From, fromSuffix) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
target := g.Nodes[edge.To]
|
|
245
|
+
if target == nil {
|
|
246
|
+
t.Fatalf("%s cited %s, which is no node", fromSuffix, edge.To)
|
|
247
|
+
}
|
|
248
|
+
if target.Simple != name {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
found++
|
|
252
|
+
if !target.External {
|
|
253
|
+
t.Fatalf("%s is a library declaration and must be an external boundary leaf", edge.To)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if found != 1 {
|
|
257
|
+
t.Fatalf("%s -> external %s edges = %d, want 1", fromSuffix, name, found)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// assertNoNamespaceNode fails when the graph holds a node for this namespace,
|
|
262
|
+
// or any doc_ref edge leaving one.
|
|
263
|
+
//
|
|
264
|
+
// A suffix match cannot express this: every node id ends in its kind, so
|
|
265
|
+
// "#Documented2:" is a suffix of nothing and an assertion written that way
|
|
266
|
+
// passes against an implementation that records the namespace and cites from
|
|
267
|
+
// it — the exact defect the pairing exists to catch.
|
|
268
|
+
func assertNoNamespaceNode(t *testing.T, g *Graph, infix string) {
|
|
269
|
+
t.Helper()
|
|
270
|
+
for id := range g.Nodes {
|
|
271
|
+
if strings.Contains(id, infix) {
|
|
272
|
+
t.Fatalf("graph recorded %s; a namespace is modelled as no node, so its documentation has no host", id)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for _, edge := range g.Edges {
|
|
276
|
+
if edge.Kind == EdgeDocRef && strings.Contains(edge.From, infix) {
|
|
277
|
+
t.Fatalf("a namespace node cited %s", edge.To)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// assertNodeExists fails unless the graph holds a node whose id ends this way.
|
|
283
|
+
// It guards the same-name case from passing because the rival never existed.
|
|
284
|
+
func assertNodeExists(t *testing.T, g *Graph, suffix string) {
|
|
285
|
+
t.Helper()
|
|
286
|
+
for id := range g.Nodes {
|
|
287
|
+
if suffixMatch(id, suffix) {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
t.Fatalf("the graph holds no %s, so a resolution asserted against it proves nothing", suffix)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// assertNoDocRefTo fails when a doc_ref edge runs between these two nodes.
|
|
295
|
+
func assertNoDocRefTo(t *testing.T, g *Graph, fromSuffix, toSuffix string) {
|
|
296
|
+
t.Helper()
|
|
297
|
+
for _, edge := range g.Edges {
|
|
298
|
+
if edge.Kind == EdgeDocRef &&
|
|
299
|
+
suffixMatch(edge.From, fromSuffix) &&
|
|
300
|
+
suffixMatch(edge.To, toSuffix) {
|
|
301
|
+
t.Fatalf("%s must not name %s", fromSuffix, toSuffix)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// assertDocRef fails unless exactly one doc_ref edge runs between the two nodes
|
|
307
|
+
// whose ids end with these suffixes, carrying a span.
|
|
308
|
+
func assertDocRef(t *testing.T, g *Graph, fromSuffix, toSuffix string) {
|
|
309
|
+
t.Helper()
|
|
310
|
+
found := 0
|
|
311
|
+
for _, edge := range g.Edges {
|
|
312
|
+
if edge.Kind != EdgeDocRef {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
if suffixMatch(edge.From, fromSuffix) && suffixMatch(edge.To, toSuffix) {
|
|
316
|
+
found++
|
|
317
|
+
if edge.Pos < 0 || edge.End <= edge.Pos {
|
|
318
|
+
t.Fatalf("%s -> %s carries no span (%d..%d)", fromSuffix, toSuffix, edge.Pos, edge.End)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if found != 1 {
|
|
323
|
+
t.Fatalf("%s -> %s doc_ref edges = %d, want 1", fromSuffix, toSuffix, found)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// assertNoDocRef fails when any doc_ref edge leaves the named node.
|
|
328
|
+
func assertNoDocRef(t *testing.T, g *Graph, fromSuffix string) {
|
|
329
|
+
t.Helper()
|
|
330
|
+
for _, edge := range g.Edges {
|
|
331
|
+
if edge.Kind == EdgeDocRef && suffixMatch(edge.From, fromSuffix) {
|
|
332
|
+
t.Fatalf("%s produced an unexpected doc_ref edge to %s", fromSuffix, edge.To)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|