ttsc 0.19.3 → 0.20.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/README.md +7 -5
- package/cmd/graphdump/main.go +2 -4
- package/cmd/ttsc/filter.go +12 -4
- package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
- package/cmd/ttsc/flags_gen.go +3 -3
- package/cmd/ttscgraph/dump.go +5 -7
- package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
- package/cmd/ttscgraph/serve.go +58 -308
- package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
- package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
- package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
- package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
- package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
- package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
- package/driver/graph.go +12 -6
- package/driver/lsp.go +30 -0
- package/driver/program.go +37 -1
- package/driver/resolution_candidates.go +819 -0
- package/driver/windowsjunction/windowsjunction.go +40 -0
- package/go.mod +2 -0
- package/internal/graph/build.go +45 -11
- package/internal/graph/dump.go +276 -100
- package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
- package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
- package/internal/graph/dump_paths.go +172 -0
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
- package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
- package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
- package/internal/graph/edges.go +208 -0
- package/internal/graph/graph.go +143 -5
- package/internal/graph/line_model.go +122 -0
- package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
- package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
- package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
- package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
- package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
- package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
- package/internal/graph/object_members.go +229 -0
- package/internal/graph/provenance.go +88 -30
- package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
- package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
- package/internal/graphsymbols/provider.go +26 -91
- package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
- package/internal/lspserver/lsp_completion.go +109 -0
- package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
- package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
- package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
- package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
- package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
- package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
- package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
- package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
- package/internal/lspserver/lsp_completion_request.go +455 -0
- package/internal/lspserver/lsp_completion_scope.go +530 -0
- package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
- package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
- package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
- package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
- package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
- package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
- package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
- package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
- package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
- package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
- package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
- package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
- package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
- package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
- package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
- package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
- package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
- package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
- package/internal/lspserver/lsp_native_plugin_source.go +315 -2
- package/internal/lspserver/lsp_native_resident.go +304 -0
- package/internal/lspserver/lsp_plugin.go +85 -8
- package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
- package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
- package/internal/lspserver/lsp_proxy.go +601 -9
- package/internal/lspserver/lsp_refresh.go +70 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
- package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
- package/internal/lspserver/lsp_server.go +13 -1
- package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
- package/lib/TtscCompiler.d.ts +3 -1
- package/lib/TtscCompiler.js +5 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.d.ts +13 -3
- package/lib/TtscService.js +6 -6
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
- package/lib/compiler/internal/project/readConfigJson.js +208 -0
- package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -149
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
- package/lib/compiler/internal/residentTransformProcess.js +159 -39
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +6 -0
- package/lib/compiler/internal/runBuild.js +143 -44
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -2
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +19 -0
- package/lib/flags/parser.js +78 -36
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +49 -11
- package/lib/flags/schema.js +94 -22
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +107 -69
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +28 -41
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
- package/lib/launcher/internal/runtimeHooks.js +18 -1
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +43 -0
- package/lib/launcher/internal/watchTopology.js +360 -0
- package/lib/launcher/internal/watchTopology.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
- package/lib/plugin/internal/buildSourcePlugin.js +247 -61
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +63 -2
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
- package/package.json +8 -8
- package/shim/ast/shim.go +48 -3
- package/shim/astnav/go.mod +14 -0
- package/shim/astnav/go.sum +20 -0
- package/shim/astnav/shim.go +15 -0
- package/shim/checker/shim.go +100 -0
- package/shim/core/shim.go +47 -0
- package/shim/diagnosticwriter/lint.go +50 -0
- package/src/TtscCompiler.ts +5 -1
- package/src/TtscService.ts +37 -10
- package/src/compiler/internal/project/readConfigJson.ts +209 -0
- package/src/compiler/internal/project/readProjectConfig.ts +62 -158
- package/src/compiler/internal/residentTransformProcess.ts +197 -39
- package/src/compiler/internal/runBuild.ts +169 -47
- package/src/compiler/internal/startResidentTransform.ts +12 -1
- package/src/compiler/internal/transformProjectInMemory.ts +12 -1
- package/src/flags/parser.ts +113 -42
- package/src/flags/schema.ts +113 -31
- package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
- package/src/launcher/internal/runTtsc.ts +117 -68
- package/src/launcher/internal/runTtsx.ts +29 -44
- package/src/launcher/internal/runtimeHooks.ts +30 -2
- package/src/launcher/internal/watchTopology.ts +466 -0
- package/src/plugin/internal/buildSourcePlugin.ts +326 -64
- package/src/plugin/internal/loadProjectPlugins.ts +80 -2
- package/src/structures/ITtscCompilerTransformation.ts +17 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
- package/tools/shim_audit/baseline.json +0 -3
- package/tools/shim_audit/main.go +42 -14
- package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
- package/utility/flags_gen.go +3 -3
- package/utility/host.go +9 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Package windowsjunction creates a Windows directory junction without
|
|
2
|
+
// interpolating filesystem paths into cmd.exe syntax.
|
|
3
|
+
package windowsjunction
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const (
|
|
13
|
+
linkEnvironment = "TTSC_WINDOWS_JUNCTION_LINK"
|
|
14
|
+
targetEnvironment = "TTSC_WINDOWS_JUNCTION_TARGET"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Create makes link a directory junction that points to target.
|
|
18
|
+
func Create(link, target string) error {
|
|
19
|
+
// mklink is a cmd.exe builtin. Feed a constant command over stdin and use
|
|
20
|
+
// delayed environment expansion so Go never has to quote a command string
|
|
21
|
+
// for cmd.exe. Delayed values are substituted after cmd has classified the
|
|
22
|
+
// command's metacharacters, keeping every character in each path as data.
|
|
23
|
+
cmd := exec.Command("cmd.exe", "/d", "/q", "/v:on")
|
|
24
|
+
cmd.Stdin = strings.NewReader(
|
|
25
|
+
"mklink /J \"!TTSC_WINDOWS_JUNCTION_LINK!\" \"!TTSC_WINDOWS_JUNCTION_TARGET!\"\r\nexit /b !errorlevel!\r\n",
|
|
26
|
+
)
|
|
27
|
+
cmd.Env = append(
|
|
28
|
+
os.Environ(),
|
|
29
|
+
linkEnvironment+"="+link,
|
|
30
|
+
targetEnvironment+"="+target,
|
|
31
|
+
)
|
|
32
|
+
if out, err := cmd.CombinedOutput(); err != nil {
|
|
33
|
+
return fmt.Errorf(
|
|
34
|
+
"mklink /J failed: %v: %s",
|
|
35
|
+
err,
|
|
36
|
+
strings.TrimSpace(string(out)),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return nil
|
|
40
|
+
}
|
package/go.mod
CHANGED
|
@@ -8,6 +8,7 @@ go 1.26
|
|
|
8
8
|
// stabilizes.
|
|
9
9
|
replace (
|
|
10
10
|
github.com/microsoft/typescript-go/shim/ast => ./shim/ast
|
|
11
|
+
github.com/microsoft/typescript-go/shim/astnav => ./shim/astnav
|
|
11
12
|
github.com/microsoft/typescript-go/shim/bundled => ./shim/bundled
|
|
12
13
|
github.com/microsoft/typescript-go/shim/checker => ./shim/checker
|
|
13
14
|
github.com/microsoft/typescript-go/shim/compiler => ./shim/compiler
|
|
@@ -26,6 +27,7 @@ replace (
|
|
|
26
27
|
|
|
27
28
|
require (
|
|
28
29
|
github.com/microsoft/typescript-go/shim/ast v0.0.0
|
|
30
|
+
github.com/microsoft/typescript-go/shim/astnav v0.0.0
|
|
29
31
|
github.com/microsoft/typescript-go/shim/bundled v0.0.0
|
|
30
32
|
github.com/microsoft/typescript-go/shim/checker v0.0.0
|
|
31
33
|
github.com/microsoft/typescript-go/shim/compiler v0.0.0
|
package/internal/graph/build.go
CHANGED
|
@@ -28,12 +28,22 @@ func Build(prog *driver.Program) *Graph {
|
|
|
28
28
|
return g
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// SourceTexts maps
|
|
32
|
-
//
|
|
31
|
+
// SourceTexts maps every program source to the resident checker text.
|
|
32
|
+
// Declaration and virtual bundled files are included because external graph
|
|
33
|
+
// leaves still carry facts and spans the source manifest must attest to.
|
|
33
34
|
func SourceTexts(prog *driver.Program) map[string]string {
|
|
34
|
-
|
|
35
|
+
if prog == nil || prog.TSProgram == nil {
|
|
36
|
+
return map[string]string{}
|
|
37
|
+
}
|
|
38
|
+
// Discarded on purpose: this returns text, not an error channel. A failed
|
|
39
|
+
// apply reaches the dump through `NewDiagnostics`, which asks `Diagnostics`.
|
|
40
|
+
_ = prog.ApplyLinkedPlugins()
|
|
41
|
+
files := prog.TSProgram.SourceFiles()
|
|
35
42
|
out := make(map[string]string, len(files))
|
|
36
43
|
for _, file := range files {
|
|
44
|
+
if file == nil {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
37
47
|
out[file.FileName()] = file.Text()
|
|
38
48
|
}
|
|
39
49
|
return out
|
|
@@ -114,6 +124,7 @@ func collectVariables(g *Graph, path string, statement *shimast.Node) {
|
|
|
114
124
|
}
|
|
115
125
|
for _, binding := range list.Declarations.Nodes {
|
|
116
126
|
addNode(g, path, binding, NodeVariable)
|
|
127
|
+
g.collectObjectMembers(path, binding)
|
|
117
128
|
collectClosures(g, path, binding)
|
|
118
129
|
}
|
|
119
130
|
}
|
|
@@ -398,18 +409,41 @@ func putDeclaredNode(g *Graph, path, name string, kind NodeKind, declaration *sh
|
|
|
398
409
|
}
|
|
399
410
|
}
|
|
400
411
|
g.Nodes[id] = &Node{
|
|
401
|
-
ID:
|
|
402
|
-
Name:
|
|
403
|
-
Simple:
|
|
404
|
-
Kind:
|
|
405
|
-
File:
|
|
406
|
-
Pos:
|
|
407
|
-
End:
|
|
408
|
-
|
|
412
|
+
ID: id,
|
|
413
|
+
Name: name,
|
|
414
|
+
Simple: simpleName(declaration.Symbol()),
|
|
415
|
+
Kind: kind,
|
|
416
|
+
File: path,
|
|
417
|
+
Pos: declaration.Pos(),
|
|
418
|
+
End: declaration.End(),
|
|
419
|
+
SignatureEnd: declarationSignatureEnd(declaration),
|
|
420
|
+
Modifiers: declarationModifiers(declaration),
|
|
409
421
|
}
|
|
410
422
|
g.bodyNodes[id] = hasBody
|
|
411
423
|
}
|
|
412
424
|
|
|
425
|
+
// declarationSignatureEnd is the offset where a declaration's head ends, or 0
|
|
426
|
+
// when this declaration has no body to bound.
|
|
427
|
+
//
|
|
428
|
+
// It reuses the boundary walk the object-member outline already uses, which
|
|
429
|
+
// stops at the construct that opens a body — a block, an object or array
|
|
430
|
+
// literal, a class expression, or an arrow's `=>`. Reusing it keeps one
|
|
431
|
+
// definition of "where the head ends" for both outlines rather than two that
|
|
432
|
+
// can disagree.
|
|
433
|
+
//
|
|
434
|
+
// A boundary at or before the declaration's own start, or past its end, is no
|
|
435
|
+
// boundary; those report 0 and leave the consumer on its existing behavior.
|
|
436
|
+
func declarationSignatureEnd(declaration *shimast.Node) int {
|
|
437
|
+
if declaration == nil {
|
|
438
|
+
return 0
|
|
439
|
+
}
|
|
440
|
+
boundary, found := sourceBodyBoundary(declaration)
|
|
441
|
+
if !found || boundary.end <= declaration.Pos() || boundary.end > declaration.End() {
|
|
442
|
+
return 0
|
|
443
|
+
}
|
|
444
|
+
return boundary.end
|
|
445
|
+
}
|
|
446
|
+
|
|
413
447
|
// declarationModifiers maps a declaration's combined modifier flags onto the
|
|
414
448
|
// wire-string subset of the TtscGraphNodeModifier union. Only flags with an
|
|
415
449
|
// exact union member are emitted, in a stable order; an unknown string would
|
package/internal/graph/dump.go
CHANGED
|
@@ -4,7 +4,6 @@ import (
|
|
|
4
4
|
"bufio"
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"io"
|
|
7
|
-
"path/filepath"
|
|
8
7
|
"sort"
|
|
9
8
|
"strings"
|
|
10
9
|
)
|
|
@@ -16,11 +15,12 @@ import (
|
|
|
16
15
|
//
|
|
17
16
|
// - internal node kinds map straight through (NodeTypeAlias is already "type");
|
|
18
17
|
// - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
|
|
19
|
-
// "renders",
|
|
18
|
+
// "renders", EdgeHeritage into "extends" / "implements", and
|
|
19
|
+
// EdgeMemberRelation into "implements" / "overrides";
|
|
20
20
|
// - byte spans become 1-based line/col Evidence ranges;
|
|
21
21
|
// - decorator facts ride on their target node;
|
|
22
|
-
// - file paths
|
|
23
|
-
// deterministic and diffable.
|
|
22
|
+
// - file paths use one portable, injective project coordinate and the output
|
|
23
|
+
// is sorted, so the dump is deterministic and diffable.
|
|
24
24
|
//
|
|
25
25
|
// Structural derivations the schema also defines (file nodes, contains/exports
|
|
26
26
|
// edges) are left to the TypeScript loader, which has the node set in hand and
|
|
@@ -65,24 +65,42 @@ type DumpEnumMember struct {
|
|
|
65
65
|
Value string `json:"value,omitempty"`
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// DumpObjectMember is one direct object-literal member carried on its variable
|
|
69
|
+
// node. Line and Signature are rendered from the same Program-owned source text
|
|
70
|
+
// as the node evidence, so the outline cannot race a later disk write.
|
|
71
|
+
type DumpObjectMember struct {
|
|
72
|
+
Name string `json:"name"`
|
|
73
|
+
Kind string `json:"kind"`
|
|
74
|
+
Line int `json:"line,omitempty"`
|
|
75
|
+
Signature string `json:"signature,omitempty"`
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
// DumpNode is the wire shape of a graph node. Lowercase json keys are the
|
|
69
79
|
// contract; the Go field names are not.
|
|
70
80
|
type DumpNode struct {
|
|
71
|
-
ID
|
|
72
|
-
Kind
|
|
73
|
-
Name
|
|
74
|
-
QualifiedName
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
ID string `json:"id"`
|
|
82
|
+
Kind string `json:"kind"`
|
|
83
|
+
Name string `json:"name"`
|
|
84
|
+
QualifiedName string `json:"qualifiedName,omitempty"`
|
|
85
|
+
// Signature is the declaration head, cut where the compiler says the body
|
|
86
|
+
// opens. A consumer that reconstructed it by scanning physical lines both
|
|
87
|
+
// leaked implementation text when a declaration shared its line with its body
|
|
88
|
+
// and stopped early when the head itself contained a brace — a type-literal
|
|
89
|
+
// parameter, an object return type, a destructured parameter. Neither is a
|
|
90
|
+
// guess the consumer can win, so the producer renders it here.
|
|
91
|
+
Signature string `json:"signature,omitempty"`
|
|
92
|
+
File string `json:"file"`
|
|
93
|
+
External bool `json:"external"`
|
|
94
|
+
Ignored bool `json:"ignored,omitempty"`
|
|
95
|
+
Exported bool `json:"exported,omitempty"`
|
|
96
|
+
Closure bool `json:"closure,omitempty"`
|
|
97
|
+
Modifiers []string `json:"modifiers,omitempty"`
|
|
98
|
+
Literals []string `json:"literals,omitempty"`
|
|
99
|
+
EnumMembers []DumpEnumMember `json:"enumMembers,omitempty"`
|
|
100
|
+
ObjectMembers []DumpObjectMember `json:"objectMembers,omitempty"`
|
|
101
|
+
Evidence *DumpEvidence `json:"evidence,omitempty"`
|
|
102
|
+
Implementation *DumpEvidence `json:"implementation,omitempty"`
|
|
103
|
+
Decorators []DumpDecorator `json:"decorators,omitempty"`
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
// DumpEdge is the wire shape of a graph edge. Lowercase json keys are the
|
|
@@ -128,12 +146,13 @@ type DumpOrigin struct {
|
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
// NewDump projects a built graph onto the export shape. project is the absolute
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
149
|
+
// root of the portable path coordinate; ignored is the git-ignored source set
|
|
150
|
+
// (nil for a non-git project); sources maps a source file's physical path to its
|
|
151
|
+
// text so byte spans become line/col evidence (nil omits evidence); origin is
|
|
152
|
+
// the snapshot evidence that proves where the facts came from. It returns an
|
|
153
|
+
// error before serialization when a path is on another filesystem root or two
|
|
154
|
+
// physical sources would collide at one wire identity.
|
|
155
|
+
func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) (Dump, error) {
|
|
137
156
|
ctx := newDumpContext(project, sources)
|
|
138
157
|
|
|
139
158
|
// Decorators ride on their target node; group by the internal node id before
|
|
@@ -173,6 +192,8 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
173
192
|
// the MCP layer is what applies a response cap and marks it.
|
|
174
193
|
Literals: n.Literals,
|
|
175
194
|
EnumMembers: dumpEnumMembers(n.EnumMembers),
|
|
195
|
+
ObjectMembers: ctx.objectMembers(n),
|
|
196
|
+
Signature: ctx.declarationSignature(n),
|
|
176
197
|
Evidence: withoutFile(ctx.evidence(n.File, n.Pos, n.End)),
|
|
177
198
|
Implementation: ctx.evidence(n.ImplementationFile, n.ImplementationPos, n.ImplementationEnd),
|
|
178
199
|
Decorators: decByNode[n.ID],
|
|
@@ -201,7 +222,7 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
201
222
|
|
|
202
223
|
// The schema version describes this code, not the caller's belief about it,
|
|
203
224
|
// so stamp it here rather than trusting what came in.
|
|
204
|
-
provenance := origin.Provenance
|
|
225
|
+
provenance := ctx.provenance(origin.Provenance)
|
|
205
226
|
provenance.SchemaVersion = DumpSchemaVersion
|
|
206
227
|
|
|
207
228
|
// A nil slice encodes as JSON null, and null is not an empty list: a reader
|
|
@@ -220,19 +241,23 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
|
|
|
220
241
|
provenance.Universe.Roots = []RootFile{}
|
|
221
242
|
}
|
|
222
243
|
|
|
223
|
-
diagnostics := origin.Diagnostics
|
|
244
|
+
diagnostics := ctx.diagnostics(origin.Diagnostics)
|
|
224
245
|
if diagnostics == nil {
|
|
225
246
|
diagnostics = []Diagnostic{}
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
|
|
249
|
+
dump := Dump{
|
|
229
250
|
Project: project,
|
|
230
|
-
Tsconfig: tsconfig,
|
|
251
|
+
Tsconfig: ctx.rel(tsconfig),
|
|
231
252
|
Provenance: provenance,
|
|
232
253
|
Diagnostics: diagnostics,
|
|
233
254
|
Nodes: nodes,
|
|
234
255
|
Edges: edges,
|
|
235
256
|
}
|
|
257
|
+
if err := ctx.pathError(); err != nil {
|
|
258
|
+
return Dump{}, err
|
|
259
|
+
}
|
|
260
|
+
return dump, nil
|
|
236
261
|
}
|
|
237
262
|
|
|
238
263
|
// dumpEnumMembers projects an enum's members onto the wire shape, nil for a
|
|
@@ -248,10 +273,42 @@ func dumpEnumMembers(members []EnumMember) []DumpEnumMember {
|
|
|
248
273
|
return out
|
|
249
274
|
}
|
|
250
275
|
|
|
276
|
+
// objectMembers projects AST-owned object member identity and snapshot-owned
|
|
277
|
+
// display text. A missing source omits only line/signature; identity still came
|
|
278
|
+
// from the AST and therefore remains sound.
|
|
279
|
+
func (c *dumpContext) objectMembers(node *Node) []DumpObjectMember {
|
|
280
|
+
if len(node.ObjectMembers) == 0 {
|
|
281
|
+
return nil
|
|
282
|
+
}
|
|
283
|
+
out := make([]DumpObjectMember, 0, len(node.ObjectMembers))
|
|
284
|
+
for _, member := range node.ObjectMembers {
|
|
285
|
+
dumped := DumpObjectMember{
|
|
286
|
+
Name: member.Name,
|
|
287
|
+
Kind: objectMemberWireKind(member.Kind),
|
|
288
|
+
}
|
|
289
|
+
if evidence := c.evidence(node.File, member.Pos, member.End); evidence != nil {
|
|
290
|
+
dumped.Line = evidence.StartLine
|
|
291
|
+
}
|
|
292
|
+
dumped.Signature = c.objectMemberSignature(node.File, member)
|
|
293
|
+
out = append(out, dumped)
|
|
294
|
+
}
|
|
295
|
+
return out
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func objectMemberWireKind(kind NodeKind) string {
|
|
299
|
+
if kind == NodeMethod {
|
|
300
|
+
return "method"
|
|
301
|
+
}
|
|
302
|
+
return "property"
|
|
303
|
+
}
|
|
304
|
+
|
|
251
305
|
// MarshalDump serializes a built graph to the export JSON, indented when pretty.
|
|
252
306
|
// See NewDump for the parameters.
|
|
253
307
|
func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
|
|
254
|
-
d := NewDump(g, project, tsconfig, ignored, sources, origin)
|
|
308
|
+
d, err := NewDump(g, project, tsconfig, ignored, sources, origin)
|
|
309
|
+
if err != nil {
|
|
310
|
+
return nil, err
|
|
311
|
+
}
|
|
255
312
|
if pretty {
|
|
256
313
|
return json.MarshalIndent(d, "", " ")
|
|
257
314
|
}
|
|
@@ -267,12 +324,16 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
|
|
|
267
324
|
// copy of it held live beside the first — half a gigabyte of peak heap that
|
|
268
325
|
// bought nothing, because the bytes were already exactly what stdout wanted.
|
|
269
326
|
func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) error {
|
|
327
|
+
dump, err := NewDump(g, project, tsconfig, ignored, sources, origin)
|
|
328
|
+
if err != nil {
|
|
329
|
+
return err
|
|
330
|
+
}
|
|
270
331
|
buffered := bufio.NewWriterSize(w, 1<<20)
|
|
271
332
|
encoder := json.NewEncoder(buffered)
|
|
272
333
|
if pretty {
|
|
273
334
|
encoder.SetIndent("", " ")
|
|
274
335
|
}
|
|
275
|
-
if err := encoder.Encode(
|
|
336
|
+
if err := encoder.Encode(dump); err != nil {
|
|
276
337
|
return err
|
|
277
338
|
}
|
|
278
339
|
return buffered.Flush()
|
|
@@ -310,6 +371,8 @@ func wireEdgeKind(kind EdgeKind, origin string) string {
|
|
|
310
371
|
return "extends"
|
|
311
372
|
}
|
|
312
373
|
return "implements"
|
|
374
|
+
case EdgeMemberRelation:
|
|
375
|
+
return origin
|
|
313
376
|
default:
|
|
314
377
|
return string(kind)
|
|
315
378
|
}
|
|
@@ -335,41 +398,30 @@ func nodeNames(n *Node) (simple, qualified string) {
|
|
|
335
398
|
return n.Simple, n.Name
|
|
336
399
|
}
|
|
337
400
|
|
|
338
|
-
// dumpContext
|
|
339
|
-
//
|
|
340
|
-
//
|
|
401
|
+
// dumpContext maps paths and turns byte spans into line/col evidence, caching a
|
|
402
|
+
// per-file line index so a large file's many edges cost O(log n) each instead
|
|
403
|
+
// of a re-scan.
|
|
341
404
|
type dumpContext struct {
|
|
342
|
-
|
|
405
|
+
paths *dumpPathMapper
|
|
343
406
|
sources map[string]string
|
|
344
407
|
lines map[string]lineStarts
|
|
345
408
|
}
|
|
346
409
|
|
|
347
410
|
func newDumpContext(project string, sources map[string]string) *dumpContext {
|
|
348
411
|
return &dumpContext{
|
|
349
|
-
|
|
412
|
+
paths: newDumpPathMapper(project),
|
|
350
413
|
sources: sources,
|
|
351
414
|
lines: map[string]lineStarts{},
|
|
352
415
|
}
|
|
353
416
|
}
|
|
354
417
|
|
|
355
|
-
// rel
|
|
356
|
-
// node_modules-relative tail so a dependency leaf stays readable.
|
|
418
|
+
// rel delegates every identity-bearing path to the dump's one cached mapper.
|
|
357
419
|
func (c *dumpContext) rel(file string) string {
|
|
358
|
-
|
|
359
|
-
if c.project != "" {
|
|
360
|
-
if f == c.project {
|
|
361
|
-
return ""
|
|
362
|
-
}
|
|
363
|
-
if strings.HasPrefix(f, c.project+"/") {
|
|
364
|
-
return f[len(c.project)+1:]
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if i := strings.LastIndex(f, "/node_modules/"); i >= 0 {
|
|
368
|
-
return f[i+1:]
|
|
369
|
-
}
|
|
370
|
-
return f
|
|
420
|
+
return c.paths.mapPath(file)
|
|
371
421
|
}
|
|
372
422
|
|
|
423
|
+
func (c *dumpContext) pathError() error { return c.paths.err() }
|
|
424
|
+
|
|
373
425
|
// relID relativizes the path portion of a node id ("path#qualifiedName:kind").
|
|
374
426
|
// An id with no path (a future virtual node) is returned unchanged.
|
|
375
427
|
//
|
|
@@ -377,17 +429,15 @@ func (c *dumpContext) rel(file string) string {
|
|
|
377
429
|
// both relativize. Every other node is named by a symbol, which passes through
|
|
378
430
|
// untouched.
|
|
379
431
|
func (c *dumpContext) relID(id string) string {
|
|
380
|
-
|
|
381
|
-
if
|
|
432
|
+
parts, ok := parseNodeID(id)
|
|
433
|
+
if !ok {
|
|
382
434
|
return id
|
|
383
435
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
name := strings.TrimSuffix(tail, suffix)
|
|
388
|
-
return head + "#" + c.rel(name) + suffix
|
|
436
|
+
name := parts.name
|
|
437
|
+
if parts.kind == NodeModule {
|
|
438
|
+
name = c.rel(name)
|
|
389
439
|
}
|
|
390
|
-
return
|
|
440
|
+
return nodeID(c.rel(parts.path), name, parts.kind)
|
|
391
441
|
}
|
|
392
442
|
|
|
393
443
|
// evidence builds the line/col span for a byte range in file, or nil when the
|
|
@@ -412,7 +462,7 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
|
|
|
412
462
|
// leading whitespace and doc comments before the token. Advance to the first
|
|
413
463
|
// code character so the line/column point at the declaration, not its banner
|
|
414
464
|
// or its indentation.
|
|
415
|
-
pos =
|
|
465
|
+
pos = FirstCodeOffset(text, pos)
|
|
416
466
|
sl, sc := ls.at(pos)
|
|
417
467
|
ev := &DumpEvidence{File: c.rel(file), StartLine: sl, StartCol: sc}
|
|
418
468
|
if end > pos && end <= len(text) {
|
|
@@ -421,36 +471,178 @@ func (c *dumpContext) evidence(file string, pos, end int) *DumpEvidence {
|
|
|
421
471
|
return ev
|
|
422
472
|
}
|
|
423
473
|
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
func
|
|
429
|
-
|
|
430
|
-
|
|
474
|
+
// declarationSignature renders a declaration's head from the same
|
|
475
|
+
// Program-owned source text as its evidence, so the outline cannot race a later
|
|
476
|
+
// disk write. Empty when the producer could not bound the head, which leaves the
|
|
477
|
+
// consumer on its existing line scan rather than handing it a wrong cut.
|
|
478
|
+
func (c *dumpContext) declarationSignature(n *Node) string {
|
|
479
|
+
if n == nil || n.SignatureEnd <= n.Pos || c.sources == nil {
|
|
480
|
+
return ""
|
|
481
|
+
}
|
|
482
|
+
text, ok := c.sources[n.File]
|
|
483
|
+
if !ok {
|
|
484
|
+
return ""
|
|
485
|
+
}
|
|
486
|
+
pos := FirstCodeOffset(text, n.Pos)
|
|
487
|
+
end := min(n.SignatureEnd, len(text))
|
|
488
|
+
if pos >= end {
|
|
489
|
+
return ""
|
|
490
|
+
}
|
|
491
|
+
return strings.TrimSpace(text[pos:end])
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// objectMemberSignature reproduces the compact outline details has historically
|
|
495
|
+
// returned, but slices it from Program-owned text while the dump is built
|
|
496
|
+
// instead of reopening the live file later.
|
|
497
|
+
func (c *dumpContext) objectMemberSignature(file string, member ObjectMember) string {
|
|
498
|
+
pos, end := member.Pos, member.SignatureEnd
|
|
499
|
+
if pos < 0 || end <= pos || c.sources == nil {
|
|
500
|
+
return ""
|
|
501
|
+
}
|
|
502
|
+
text, ok := c.sources[file]
|
|
503
|
+
if !ok || pos > len(text) {
|
|
504
|
+
return ""
|
|
505
|
+
}
|
|
506
|
+
pos = FirstCodeOffset(text, pos)
|
|
507
|
+
if end > len(text) {
|
|
508
|
+
end = len(text)
|
|
509
|
+
}
|
|
510
|
+
if member.SignatureTokenLen > 0 {
|
|
511
|
+
end = FirstCodeOffset(text, end)
|
|
512
|
+
end = min(end+member.SignatureTokenLen, len(text))
|
|
513
|
+
}
|
|
514
|
+
if pos >= end {
|
|
515
|
+
return ""
|
|
516
|
+
}
|
|
517
|
+
if pos >= end {
|
|
518
|
+
return ""
|
|
519
|
+
}
|
|
520
|
+
signature := strings.TrimSuffix(compactObjectMemberSignature(text[pos:end]), ",")
|
|
521
|
+
const maxObjectMemberSignatureRunes = 160
|
|
522
|
+
runes := []rune(signature)
|
|
523
|
+
if len(runes) > maxObjectMemberSignatureRunes {
|
|
524
|
+
signature = string(runes[:maxObjectMemberSignatureRunes-3]) + "..."
|
|
525
|
+
}
|
|
526
|
+
return signature
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// compactObjectMemberSignature collapses trivia outside lexical values while
|
|
530
|
+
// leaving quoted strings, template literals, regular expressions, and comments
|
|
531
|
+
// byte-for-byte intact. A display outline may be compact, but changing literal
|
|
532
|
+
// whitespace would change the declaration it claims to quote.
|
|
533
|
+
func compactObjectMemberSignature(text string) string {
|
|
534
|
+
var out strings.Builder
|
|
535
|
+
pendingSpace := false
|
|
536
|
+
for i := 0; i < len(text); {
|
|
537
|
+
if width := sourceWhitespaceWidth(text, i); width > 0 {
|
|
538
|
+
pendingSpace = out.Len() > 0
|
|
539
|
+
i += width
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
if pendingSpace {
|
|
543
|
+
out.WriteByte(' ')
|
|
544
|
+
pendingSpace = false
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
end := i + 1
|
|
548
|
+
switch text[i] {
|
|
549
|
+
case '\'', '"':
|
|
550
|
+
end = quotedSourceEnd(text, i, text[i])
|
|
551
|
+
case '`':
|
|
552
|
+
end = templateSourceEnd(text, i)
|
|
553
|
+
case '/':
|
|
554
|
+
switch {
|
|
555
|
+
case i+1 < len(text) && text[i+1] == '/':
|
|
556
|
+
end = LineCommentEnd(text, i)
|
|
557
|
+
case i+1 < len(text) && text[i+1] == '*':
|
|
558
|
+
end = blockCommentEnd(text, i)
|
|
559
|
+
default:
|
|
560
|
+
if candidate := regularExpressionEnd(text, i); candidate > i {
|
|
561
|
+
end = candidate
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
out.WriteString(text[i:end])
|
|
566
|
+
i = end
|
|
567
|
+
}
|
|
568
|
+
return strings.TrimSpace(out.String())
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
func quotedSourceEnd(text string, start int, quote byte) int {
|
|
572
|
+
escaped := false
|
|
573
|
+
for i := start + 1; i < len(text); i++ {
|
|
574
|
+
if escaped {
|
|
575
|
+
escaped = false
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
if text[i] == '\\' {
|
|
579
|
+
escaped = true
|
|
580
|
+
continue
|
|
581
|
+
}
|
|
582
|
+
if text[i] == quote {
|
|
583
|
+
return i + 1
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return len(text)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// templateSourceEnd returns the last unescaped backtick in the member span.
|
|
590
|
+
// This deliberately treats substitutions and nested templates as one protected
|
|
591
|
+
// lexical region: preserving a little extra spacing is safer than compacting
|
|
592
|
+
// whitespace that belongs to either template's value.
|
|
593
|
+
func templateSourceEnd(text string, start int) int {
|
|
594
|
+
end := len(text)
|
|
595
|
+
for i := len(text) - 1; i > start; i-- {
|
|
596
|
+
if text[i] == '`' && !sourceByteEscaped(text, i) {
|
|
597
|
+
return i + 1
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return end
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
func sourceByteEscaped(text string, pos int) bool {
|
|
604
|
+
slashes := 0
|
|
605
|
+
for i := pos - 1; i >= 0 && text[i] == '\\'; i-- {
|
|
606
|
+
slashes++
|
|
607
|
+
}
|
|
608
|
+
return slashes%2 == 1
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
func blockCommentEnd(text string, start int) int {
|
|
612
|
+
if end := strings.Index(text[start+2:], "*/"); end >= 0 {
|
|
613
|
+
return start + 2 + end + 2
|
|
614
|
+
}
|
|
615
|
+
return len(text)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// regularExpressionEnd conservatively protects a slash-delimited region when
|
|
619
|
+
// one closes on the same source line. Division can look the same without parser
|
|
620
|
+
// context; preserving its spaces is harmless, while compacting a real regular
|
|
621
|
+
// expression would change its pattern.
|
|
622
|
+
func regularExpressionEnd(text string, start int) int {
|
|
623
|
+
escaped := false
|
|
624
|
+
inClass := false
|
|
625
|
+
for i := start + 1; i < len(text); i++ {
|
|
431
626
|
switch {
|
|
432
|
-
case text
|
|
627
|
+
case lineTerminatorWidth(text, i) > 0:
|
|
628
|
+
return start
|
|
629
|
+
case escaped:
|
|
630
|
+
escaped = false
|
|
631
|
+
case text[i] == '\\':
|
|
632
|
+
escaped = true
|
|
633
|
+
case text[i] == '[':
|
|
634
|
+
inClass = true
|
|
635
|
+
case text[i] == ']':
|
|
636
|
+
inClass = false
|
|
637
|
+
case text[i] == '/' && !inClass:
|
|
433
638
|
i++
|
|
434
|
-
|
|
435
|
-
i += 2
|
|
436
|
-
for i < len(text) && text[i] != '\n' {
|
|
639
|
+
for i < len(text) && ((text[i] >= 'a' && text[i] <= 'z') || (text[i] >= 'A' && text[i] <= 'Z')) {
|
|
437
640
|
i++
|
|
438
641
|
}
|
|
439
|
-
case text[i] == '/' && i+1 < len(text) && text[i+1] == '*':
|
|
440
|
-
i += 2
|
|
441
|
-
for i+1 < len(text) && !(text[i] == '*' && text[i+1] == '/') {
|
|
442
|
-
i++
|
|
443
|
-
}
|
|
444
|
-
if i+1 < len(text) {
|
|
445
|
-
i += 2
|
|
446
|
-
} else {
|
|
447
|
-
i = len(text)
|
|
448
|
-
}
|
|
449
|
-
default:
|
|
450
642
|
return i
|
|
451
643
|
}
|
|
452
644
|
}
|
|
453
|
-
return
|
|
645
|
+
return start
|
|
454
646
|
}
|
|
455
647
|
|
|
456
648
|
// edgeEvidence is the evidence range for an edge's source expression.
|
|
@@ -473,28 +665,12 @@ func withoutFile(ev *DumpEvidence) *DumpEvidence {
|
|
|
473
665
|
return ev
|
|
474
666
|
}
|
|
475
667
|
|
|
476
|
-
// nodeFile recovers the source file path embedded in a node id
|
|
477
|
-
// ("path#qualifiedName:kind"); "" for an id without a path.
|
|
478
|
-
func nodeFile(id string) string {
|
|
479
|
-
if hash := strings.Index(id, "#"); hash >= 0 {
|
|
480
|
-
return id[:hash]
|
|
481
|
-
}
|
|
482
|
-
return ""
|
|
483
|
-
}
|
|
484
|
-
|
|
485
668
|
// lineStarts holds the byte offset of each line's start, so an offset maps to a
|
|
486
669
|
// 1-based line/column by binary search.
|
|
487
670
|
type lineStarts []int
|
|
488
671
|
|
|
489
672
|
func newLineStarts(text string) lineStarts {
|
|
490
|
-
|
|
491
|
-
starts[0] = 0
|
|
492
|
-
for i := 0; i < len(text); i++ {
|
|
493
|
-
if text[i] == '\n' {
|
|
494
|
-
starts = append(starts, i+1)
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return starts
|
|
673
|
+
return lineStarts(ECMALineStarts(text))
|
|
498
674
|
}
|
|
499
675
|
|
|
500
676
|
// at returns the 1-based line and column of a byte offset.
|