@ontrails/source 1.0.0-beta.41
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/CHANGELOG.md +53 -0
- package/README.md +80 -0
- package/package.json +29 -0
- package/src/edits.ts +57 -0
- package/src/index.ts +24 -0
- package/src/literals.ts +226 -0
- package/src/locations.ts +35 -0
- package/src/nodes.ts +650 -0
- package/src/parse.ts +55 -0
- package/src/scopes.ts +517 -0
- package/src/trails.ts +826 -0
- package/src/walk.ts +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @ontrails/source
|
|
2
|
+
|
|
3
|
+
## 1.0.0-beta.41
|
|
4
|
+
|
|
5
|
+
## 1.0.0-beta.40
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- [`35cbe28`](https://github.com/outfitter-dev/trails/commit/35cbe289db46539b3689dbf6cf8ab0e5d9a1b09c): Found `@ontrails/source` as the shared source-code AST kernel for parsing,
|
|
10
|
+
walking, locations, edits, literals, and generic Trails syntax recognition.
|
|
11
|
+
Warden, Regrade, Wayfinder, and the Trails operator now import those shared
|
|
12
|
+
mechanics from `@ontrails/source`; the legacy Warden AST route is removed by the
|
|
13
|
+
stacked hard cutover.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`3531b58`](https://github.com/outfitter-dev/trails/commit/3531b58ba5320753d6d2594257ef71bc950d28a1): Add the advisory captured-kernel Warden rule for ownership review when a public
|
|
18
|
+
subpath re-exports package internals and multiple production workspaces consume
|
|
19
|
+
that subpath, including import-then-export barrels that preserve the internal
|
|
20
|
+
binding through a local alias or default export.
|
|
21
|
+
|
|
22
|
+
Expose typed import-kind inspection from `@ontrails/source` so project rules
|
|
23
|
+
can keep erased type bindings separate from runtime exports.
|
|
24
|
+
|
|
25
|
+
- [`10f2492`](https://github.com/outfitter-dev/trails/commit/10f24928d3bc9d995abf7aa261ecf515c295855d): Own the `wayfind.outline` implementation in the Trails operator app while preserving the existing `trails wayfind file <file> --outline` CLI and MCP composition behavior, and document `@ontrails/source` as the operator's live-source analysis kernel.
|
|
26
|
+
- [`35e5fed`](https://github.com/outfitter-dev/trails/commit/35e5fedd228e498783f479f0dd502e2f3ec772b8): Fold the Wayfinder graph-read catalog into `@ontrails/topography`. Wayfind
|
|
27
|
+
remains the product, trail-id, CLI, and MCP brand, but there is no longer an
|
|
28
|
+
`@ontrails/wayfinder` package to install or import. Programmatic consumers
|
|
29
|
+
should move imports such as `wayfinderTopo`, `wayfindOverviewTrail`,
|
|
30
|
+
`loadWayfinderArtifacts`, and the Wayfinder filter/provenance types to
|
|
31
|
+
`@ontrails/topography`.
|
|
32
|
+
|
|
33
|
+
Expose that package move as a governed Regrade transition so exact
|
|
34
|
+
`@ontrails/wayfinder` imports can move safely while product vocabulary and near
|
|
35
|
+
routes remain unchanged for review. Regrade routes package manifests through
|
|
36
|
+
structured review instead of rewriting dependency keys as plain text.
|
|
37
|
+
|
|
38
|
+
The Trails operator now reads all `wayfind.*` query trails and artifact helpers
|
|
39
|
+
from `@ontrails/topography` while preserving the existing CLI/MCP schemas,
|
|
40
|
+
route IDs, output shapes, and internal trail visibility.
|
|
41
|
+
|
|
42
|
+
- [`3a65ae3`](https://github.com/outfitter-dev/trails/commit/3a65ae363e05b7589f4a9876da4346886353b48c): Rename the durable graph substrate package from `@ontrails/topographer` to
|
|
43
|
+
`@ontrails/topography` after folding Wayfind graph queries into that owner.
|
|
44
|
+
|
|
45
|
+
Update imports to `@ontrails/topography` or
|
|
46
|
+
`@ontrails/topography/backend-support`. The pre-1.0 cutover does not ship a
|
|
47
|
+
compatibility package. TopoGraph, lock, topo-store, semantic diff, and Wayfind
|
|
48
|
+
APIs keep their existing contracts, and the `trails wayfind` CLI and MCP names
|
|
49
|
+
remain unchanged.
|
|
50
|
+
|
|
51
|
+
The governed package-route transition moves legacy `@ontrails/wayfinder`
|
|
52
|
+
imports directly to `@ontrails/topography`; it does not emit the retired
|
|
53
|
+
intermediate `@ontrails/topographer` route.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @ontrails/source
|
|
2
|
+
|
|
3
|
+
Shared source-code machinery for Trails packages and repo tooling.
|
|
4
|
+
|
|
5
|
+
`source` means source code: TypeScript and JavaScript text parsed into an OXC AST. It does not mean activation source, signal source, data source, event source, or execution source.
|
|
6
|
+
|
|
7
|
+
## What It Owns
|
|
8
|
+
|
|
9
|
+
`@ontrails/source` owns reusable source-code mechanics:
|
|
10
|
+
|
|
11
|
+
- AST node guards and accessors for the OXC node shapes Trails tooling uses.
|
|
12
|
+
- `parse` and `parseWithDiagnostics` wrappers over `oxc-parser`.
|
|
13
|
+
- `walk`, parent-aware walking, and scope-aware walking over `oxc-walker`.
|
|
14
|
+
- Source locations, source edits, literal extraction, and generic Trails syntax recognition.
|
|
15
|
+
- Generic trail/entity discovery helpers such as `findTrailDefinitions`, `findImplementationBodies`, `findEntityDefinitions`, and `isImplementationCall`.
|
|
16
|
+
|
|
17
|
+
The package root is the public API. Import from `@ontrails/source`; there are no supported `/ast`, `/trails`, or `/utils` subpaths.
|
|
18
|
+
|
|
19
|
+
## Package Admission Test
|
|
20
|
+
|
|
21
|
+
`@ontrails/source` exists because the same source-code contract is reused by independent toolchain owners:
|
|
22
|
+
|
|
23
|
+
- Warden uses it to implement source-static governance without owning the parser facade.
|
|
24
|
+
- Regrade uses it for safe downstream source rewrites.
|
|
25
|
+
- The Trails operator uses it to assemble live source-file outlines.
|
|
26
|
+
- The `trails` operator uses it for draft promotion and version-lifecycle support.
|
|
27
|
+
|
|
28
|
+
The package is admitted only for reusable source machinery with at least two independent toolchain owners and a genuinely shared contract. It must not absorb product verdicts, release plans, query semantics, rendering, Warden rule policy, Regrade engines, Topography artifact assembly, or Wayfinder answer composition.
|
|
29
|
+
|
|
30
|
+
This boundary follows the package-worthiness rule in [ADR-0051: Package Ownership Follows Natural Altitude](../../docs/adr/0051-package-ownership-follows-natural-altitude.md): move code when the natural owner is above one consumer, not when a file merely feels crowded.
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
Parse source and inspect trail declarations:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { findTrailDefinitions, parse } from '@ontrails/source';
|
|
38
|
+
|
|
39
|
+
const ast = parse(
|
|
40
|
+
'example.ts',
|
|
41
|
+
"import { trail } from '@ontrails/core';\nexport const show = trail('user.show', {});\n"
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const trailIds = ast ? findTrailDefinitions(ast).map((trail) => trail.id) : [];
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Walk source with parent context:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { parse, walkWithParents } from '@ontrails/source';
|
|
51
|
+
|
|
52
|
+
const ast = parse('example.ts', 'const value = trail("demo.show", {});\n');
|
|
53
|
+
const callParents: string[] = [];
|
|
54
|
+
|
|
55
|
+
if (ast) {
|
|
56
|
+
walkWithParents(ast, (node, context) => {
|
|
57
|
+
if (node.type === 'CallExpression') {
|
|
58
|
+
callParents.push(`${context.parent?.type ?? 'root'}:${String(context.key)}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Apply source edits:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { applySourceEdits, createSourceEdit } from '@ontrails/source';
|
|
68
|
+
|
|
69
|
+
const updated = applySourceEdits('const name = "old";\n', [
|
|
70
|
+
createSourceEdit(14, 17, 'new'),
|
|
71
|
+
]);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Non-Goals
|
|
75
|
+
|
|
76
|
+
- Warden rule policy and rule-specific facts remain in `@ontrails/warden`.
|
|
77
|
+
- Regrade migration planning and execution remain in `@ontrails/regrade`.
|
|
78
|
+
- Topography graph artifacts and outline assembly remain outside this package.
|
|
79
|
+
- Wayfind query trails and answer rendering remain in `@ontrails/topography`.
|
|
80
|
+
- Runtime Trails contracts remain in `@ontrails/core`.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ontrails/source",
|
|
3
|
+
"version": "1.0.0-beta.41",
|
|
4
|
+
"description": "Shared source-code AST parsing, walking, location, edit, literal, and Trails syntax helpers.",
|
|
5
|
+
"files": [
|
|
6
|
+
"src/**/*.ts",
|
|
7
|
+
"!src/**/__tests__/**",
|
|
8
|
+
"!src/**/*.test.ts",
|
|
9
|
+
"!src/**/*.test-d.ts",
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -b",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "oxlint ./src",
|
|
23
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"oxc-parser": "^0.121.0",
|
|
27
|
+
"oxc-walker": "^1.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/edits.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Shared source-edit helpers. */
|
|
2
|
+
|
|
3
|
+
import type { SourceEdit } from './nodes.js';
|
|
4
|
+
|
|
5
|
+
export const createSourceEdit = (
|
|
6
|
+
start: number,
|
|
7
|
+
end: number,
|
|
8
|
+
replacement: string
|
|
9
|
+
): SourceEdit => ({ end, replacement, start });
|
|
10
|
+
|
|
11
|
+
export const validateSourceEdits = (
|
|
12
|
+
edits: readonly SourceEdit[],
|
|
13
|
+
sourceLength?: number
|
|
14
|
+
): readonly SourceEdit[] => {
|
|
15
|
+
const ordered = [...edits].toSorted(
|
|
16
|
+
(left, right) => left.start - right.start
|
|
17
|
+
);
|
|
18
|
+
for (let i = 0; i < ordered.length; i += 1) {
|
|
19
|
+
const edit = ordered[i];
|
|
20
|
+
if (!edit) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
!Number.isSafeInteger(edit.start) ||
|
|
25
|
+
!Number.isSafeInteger(edit.end) ||
|
|
26
|
+
edit.start < 0 ||
|
|
27
|
+
edit.end < edit.start ||
|
|
28
|
+
(sourceLength !== undefined && edit.end > sourceLength)
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`Invalid source edit range ${edit.start}-${edit.end}.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const previous = ordered[i - 1];
|
|
34
|
+
if (previous && edit.start < previous.end) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Overlapping source edits ${previous.start}-${previous.end} and ${edit.start}-${edit.end}.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return ordered;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const applySourceEdits = (
|
|
45
|
+
sourceCode: string,
|
|
46
|
+
edits: readonly SourceEdit[]
|
|
47
|
+
): string => {
|
|
48
|
+
validateSourceEdits(edits, sourceCode.length);
|
|
49
|
+
|
|
50
|
+
return [...edits]
|
|
51
|
+
.toSorted((left, right) => right.start - left.start)
|
|
52
|
+
.reduce(
|
|
53
|
+
(output, edit) =>
|
|
54
|
+
output.slice(0, edit.start) + edit.replacement + output.slice(edit.end),
|
|
55
|
+
sourceCode
|
|
56
|
+
);
|
|
57
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export * from './nodes.js';
|
|
2
|
+
export * from './parse.js';
|
|
3
|
+
export * from './walk.js';
|
|
4
|
+
export * from './scopes.js';
|
|
5
|
+
export * from './locations.js';
|
|
6
|
+
export * from './edits.js';
|
|
7
|
+
export * from './literals.js';
|
|
8
|
+
export type {
|
|
9
|
+
EntityDefinition,
|
|
10
|
+
FindEntityDefinitionsOptions,
|
|
11
|
+
FrameworkNamespaceContext,
|
|
12
|
+
TrailDefinition,
|
|
13
|
+
} from './trails.js';
|
|
14
|
+
export {
|
|
15
|
+
buildFrameworkNamespaceContext,
|
|
16
|
+
extractEntityDefinition,
|
|
17
|
+
extractTrailDefinition,
|
|
18
|
+
findEntityDefinitions,
|
|
19
|
+
findImplementationBodies,
|
|
20
|
+
findTrailDefinitions,
|
|
21
|
+
getImportSourceValue,
|
|
22
|
+
isImplementationCall,
|
|
23
|
+
isFrameworkNamespaceSource,
|
|
24
|
+
} from './trails.js';
|
package/src/literals.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** Shared literal, string, and static property-key helpers. */
|
|
2
|
+
|
|
3
|
+
import { isAstNode } from './nodes.js';
|
|
4
|
+
import type { AstNode, StringLiteralNode } from './nodes.js';
|
|
5
|
+
import { walk } from './walk.js';
|
|
6
|
+
|
|
7
|
+
export const identifierName = (node: AstNode | undefined): string | null => {
|
|
8
|
+
if (node?.type !== 'Identifier') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return (node as unknown as { name?: string }).name ?? null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Check if a node is a string literal. */
|
|
15
|
+
export const isStringLiteral = (
|
|
16
|
+
node: AstNode | undefined
|
|
17
|
+
): node is StringLiteralNode => {
|
|
18
|
+
if (!node) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (node.type === 'StringLiteral') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (node.type === 'Literal') {
|
|
25
|
+
return typeof (node as unknown as { value?: unknown }).value === 'string';
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Extract the string value from a string literal node. */
|
|
31
|
+
export const getStringValue = (node: AstNode): string | null => {
|
|
32
|
+
const val = (node as unknown as { value?: unknown }).value;
|
|
33
|
+
return typeof val === 'string' ? val : null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort resolution of `const NAME = 'value'` declarations via regex.
|
|
38
|
+
*
|
|
39
|
+
* Returns the string value if a simple `const <name> = '...'` or `"..."` is
|
|
40
|
+
* found in the source. Returns null for anything more complex. Shared between
|
|
41
|
+
* warden rules that need to resolve identifier references to signal / trail
|
|
42
|
+
* IDs at lint time.
|
|
43
|
+
*/
|
|
44
|
+
export const deriveConstString = (
|
|
45
|
+
name: string,
|
|
46
|
+
sourceCode: string
|
|
47
|
+
): string | null => {
|
|
48
|
+
const pattern = new RegExp(
|
|
49
|
+
`const\\s+${name}\\s*=\\s*(?:'([^']*)'|"([^"]*)")`
|
|
50
|
+
);
|
|
51
|
+
const match = pattern.exec(sourceCode);
|
|
52
|
+
if (!match) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return match[1] ?? match[2] ?? null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Extract a string literal value, or null when the node is not a string. */
|
|
59
|
+
export const extractStringLiteral = (
|
|
60
|
+
node: AstNode | undefined
|
|
61
|
+
): string | null =>
|
|
62
|
+
node && isStringLiteral(node) ? getStringValue(node) : null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract the cooked value from a `TemplateLiteral` with no interpolations
|
|
66
|
+
* (e.g. `` `entity.fallback` ``). Template literals with `${...}` expressions
|
|
67
|
+
* cannot be resolved at lint time and return null.
|
|
68
|
+
*
|
|
69
|
+
* Shared helper used by rules that accept both string literals and simple
|
|
70
|
+
* backtick-literal IDs (e.g. `valid-describe-refs`).
|
|
71
|
+
*/
|
|
72
|
+
const getSingleQuasi = (node: AstNode): AstNode | null => {
|
|
73
|
+
const expressions =
|
|
74
|
+
(node['expressions'] as readonly AstNode[] | undefined) ?? [];
|
|
75
|
+
if (expressions.length > 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const quasis = (node['quasis'] as readonly AstNode[] | undefined) ?? [];
|
|
79
|
+
return quasis.length === 1 ? (quasis[0] ?? null) : null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const extractPlainTemplateLiteral = (
|
|
83
|
+
node: AstNode | undefined
|
|
84
|
+
): string | null => {
|
|
85
|
+
if (!node || node.type !== 'TemplateLiteral') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const quasi = getSingleQuasi(node);
|
|
89
|
+
if (!quasi) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const cooked = (quasi as unknown as { value?: { cooked?: unknown } }).value
|
|
93
|
+
?.cooked;
|
|
94
|
+
return typeof cooked === 'string' ? cooked : null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract a string value from either a string literal or a plain template
|
|
99
|
+
* literal (no `${...}` expressions). Returns null for anything else.
|
|
100
|
+
*/
|
|
101
|
+
export const extractStringOrTemplateLiteral = (
|
|
102
|
+
node: AstNode | undefined
|
|
103
|
+
): string | null =>
|
|
104
|
+
extractStringLiteral(node) ?? extractPlainTemplateLiteral(node);
|
|
105
|
+
|
|
106
|
+
export interface StringLiteralMatch {
|
|
107
|
+
readonly end: number;
|
|
108
|
+
readonly node: AstNode;
|
|
109
|
+
readonly start: number;
|
|
110
|
+
readonly value: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const findStringLiterals = (
|
|
114
|
+
ast: AstNode,
|
|
115
|
+
predicate?: (value: string, node: AstNode) => boolean
|
|
116
|
+
): StringLiteralMatch[] => {
|
|
117
|
+
const matches: StringLiteralMatch[] = [];
|
|
118
|
+
|
|
119
|
+
walk(ast, (node) => {
|
|
120
|
+
if (!isStringLiteral(node)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const value = getStringValue(node);
|
|
125
|
+
if (value === null) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (predicate && !predicate(value, node)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
matches.push({
|
|
134
|
+
end: node.end,
|
|
135
|
+
node,
|
|
136
|
+
start: node.start,
|
|
137
|
+
value,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return matches;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Extract the first string argument from a CallExpression. */
|
|
145
|
+
export const extractFirstStringArg = (node: AstNode): string | null => {
|
|
146
|
+
if (node.type !== 'CallExpression') {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
151
|
+
const [firstArg] = args ?? [];
|
|
152
|
+
return extractStringLiteral(firstArg);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const extractBindingName = (
|
|
156
|
+
node: AstNode | undefined
|
|
157
|
+
): string | null => {
|
|
158
|
+
if (!node) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (node.type === 'Identifier') {
|
|
162
|
+
return identifierName(node);
|
|
163
|
+
}
|
|
164
|
+
if (node.type === 'AssignmentPattern') {
|
|
165
|
+
return identifierName((node as unknown as { left?: AstNode }).left);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const staticPropertyKeyName = (key: AstNode): string | null => {
|
|
171
|
+
if (key.type === 'Identifier') {
|
|
172
|
+
return (key as unknown as { name?: string }).name ?? null;
|
|
173
|
+
}
|
|
174
|
+
return isStringLiteral(key) ? getStringValue(key) : null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const propertyKeyName = (prop: AstNode): string | null => {
|
|
178
|
+
if (prop.type !== 'Property') {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const { computed } = prop as unknown as { computed?: boolean };
|
|
182
|
+
if (computed) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const key = prop.key as AstNode | undefined;
|
|
186
|
+
return key ? staticPropertyKeyName(key) : null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
190
|
+
export const findConfigProperty = (
|
|
191
|
+
config: AstNode,
|
|
192
|
+
propertyName: string
|
|
193
|
+
): AstNode | null => {
|
|
194
|
+
if (config.type !== 'ObjectExpression') {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const properties = config['properties'] as readonly AstNode[] | undefined;
|
|
198
|
+
if (!properties) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
for (const prop of properties) {
|
|
202
|
+
if (propertyKeyName(prop) === propertyName) {
|
|
203
|
+
return prop;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Read a property key or member access identifier.
|
|
211
|
+
*
|
|
212
|
+
* Returns the identifier name for `Identifier` keys, or the underlying
|
|
213
|
+
* string literal value for computed access via `['name']` / `"name"`.
|
|
214
|
+
*/
|
|
215
|
+
export const getPropertyName = (node: unknown): string | null => {
|
|
216
|
+
if (typeof node !== 'object' || node === null) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { name } = node as { readonly name?: unknown };
|
|
221
|
+
if (typeof name === 'string') {
|
|
222
|
+
return name;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return isAstNode(node) ? extractStringLiteral(node) : null;
|
|
226
|
+
};
|
package/src/locations.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Shared source location helpers. */
|
|
2
|
+
|
|
3
|
+
import type { SourceLocation } from './nodes.js';
|
|
4
|
+
|
|
5
|
+
/** Find the byte offset's line number (1-based) in source code. */
|
|
6
|
+
export const offsetToLine = (sourceCode: string, offset: number): number => {
|
|
7
|
+
let line = 1;
|
|
8
|
+
for (let i = 0; i < offset && i < sourceCode.length; i += 1) {
|
|
9
|
+
if (sourceCode[i] === '\n') {
|
|
10
|
+
line += 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return line;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Find the byte offset's line and column (1-based) in source code. */
|
|
17
|
+
export const offsetToLineColumn = (
|
|
18
|
+
sourceCode: string,
|
|
19
|
+
offset: number
|
|
20
|
+
): SourceLocation => {
|
|
21
|
+
let line = 1;
|
|
22
|
+
let column = 1;
|
|
23
|
+
const limit = Math.min(Math.max(offset, 0), sourceCode.length);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < limit; i += 1) {
|
|
26
|
+
if (sourceCode[i] === '\n') {
|
|
27
|
+
line += 1;
|
|
28
|
+
column = 1;
|
|
29
|
+
} else {
|
|
30
|
+
column += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { column, line };
|
|
35
|
+
};
|