do not rewrite assignments

This commit is contained in:
Oleksii Bulenok
2026-08-31 15:47:21 +02:00
parent c26da684c9
commit ab9f8df1b7
2 changed files with 53 additions and 2 deletions
@@ -78,6 +78,34 @@ describe('transform', () => {
expect(out).not.toContain('import *')
})
test('bails when the chain is a write target', () => {
const writes = [
'app.bsky.feed.like = 1',
'app.bsky.feed.like++',
'delete app.bsky.feed.like',
'for (app.bsky.feed.like of []) {}',
';[app.bsky.feed.like] = []',
';({x: app.bsky.feed.like} = {})',
]
for (const stmt of writes) {
const out = applyPlugin(
`import {app} from './lexicons'\n${stmt}\n`,
PROBE_FILE,
)
expect(out).toContain(`from './lexicons'`)
expect(out).not.toContain('import *')
}
})
test('rewrites a read of a leaf even when a sibling member is written', () => {
const out = applyPlugin(
`import {app} from './lexicons'\ndelete app.bsky.feed.like.$cached\n`,
PROBE_FILE,
)
expect(out).toContain('delete _lex_app_bsky_feed_like.$cached')
expect(out).not.toContain(`from './lexicons'`)
})
test('leaves type-only imports untouched', () => {
const src = `import type {app} from './lexicons'\nexport type T = typeof app\n`
const out = applyPlugin(src, PROBE_FILE)
+25 -2
View File
@@ -21,8 +21,8 @@
* which imports its own copy of the barrel via '../lexicons/index.js'.
*
* Correctness fallback: if any reference to a barrel binding cannot be rewritten
* (namespace used as a value, computed access, chain ending at a non-leaf), that
* binding is left on the barrel import. The result is always correct, merely
* (namespace used as a value, computed access, chain ending at a non-leaf,
* chain in a write position), that binding is left on the barrel import. The result is always correct, merely
* unshaken for that file. Set BSKY_LEXICON_IMPORTS_DEBUG=1 to log such bails.
*
* Caveat: the rewrite bakes leaf file paths into each consumer's transform
@@ -96,6 +96,28 @@ function leafFileFor(dir, segment) {
return null
}
/**
* True when the member chain is written to rather than read. Such a chain
* cannot be collapsed into a bare identifier: imports are read-only bindings
* (assignment/++ would throw where the original property write may not) and
* `delete <identifier>` is a strict-mode SyntaxError in the emitted code.
*/
function isWriteTarget(memberPath) {
const parent = memberPath.parentPath
if (!parent) return false
if (parent.isAssignmentExpression()) {
return parent.node.left === memberPath.node
}
if (parent.isUpdateExpression()) return true
if (parent.isUnaryExpression({operator: 'delete'})) return true
if (parent.isForXStatement()) return parent.node.left === memberPath.node
if (parent.isArrayPattern() || parent.isRestElement()) return true
if (parent.isObjectProperty() && parent.parentPath.isObjectPattern()) {
return parent.node.value === memberPath.node
}
return false
}
/** `rootDir\0segments\0leafFile` -> boolean */
const chainCache = new Map()
@@ -208,6 +230,7 @@ module.exports = function lexiconLeafImports(babel, options = {}) {
for (;;) {
const kind = classify(dir, segment)
if (kind === 'file') {
if (isWriteTarget(cur)) return null
return {memberPath: cur, leafFile: leafFileFor(dir, segment), segments}
}
if (kind !== 'dir') return null