GeezerVison is showing github.com — repl: fix REPL completion under unary expressions · nodejs/node@a414c1e
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/repl.js +10Lines changed: 10 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1744,6 +1744,16 @@ function findExpressionCompleteTarget(code) { | 1744 | 1744 | return findExpressionCompleteTarget(lastDeclarationInitCode); | 1745 | 1745 | } | 1746 | 1746 |
| | 1747 | + // If the last statement is an expression statement with a unary operator (delete, typeof, etc.) | | 1748 | + // we want to extract the argument for completion (e.g. for `delete obj.prop` we want `obj.prop`) | | 1749 | + if (lastBodyStatement.type === 'ExpressionStatement' && | | 1750 | + lastBodyStatement.expression.type === 'UnaryExpression' && | | 1751 | + lastBodyStatement.expression.argument) { | | 1752 | + const argument = lastBodyStatement.expression.argument; | | 1753 | + const argumentCode = code.slice(argument.start, argument.end); | | 1754 | + return findExpressionCompleteTarget(argumentCode); | | 1755 | + } | | 1756 | + | 1747 | 1757 | // If any of the above early returns haven't activated then it means that | 1748 | 1758 | // the potential complete target is the full code (e.g. the code represents | 1749 | 1759 | // a simple partial identifier, a member expression, etc...) | |
Collapse file Copy file name to clipboard +141Lines changed: 141 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +const assert = require('assert'); | | 5 | +const repl = require('repl'); | | 6 | +const { describe, it } = require('node:test'); | | 7 | + | | 8 | +// This test verifies that tab completion works correctly with unary expressions | | 9 | +// like delete, typeof, void, etc. This is a regression test for the issue where | | 10 | +// typing "delete globalThis._" and then backspacing and typing "globalThis" | | 11 | +// would cause "globalThis is not defined" error. | | 12 | + | | 13 | +describe('REPL tab completion with unary expressions', () => { | | 14 | + it('should handle delete operator correctly', (t, done) => { | | 15 | + const r = repl.start({ | | 16 | + prompt: '', | | 17 | + input: process.stdin, | | 18 | + output: process.stdout, | | 19 | + terminal: false, | | 20 | + }); | | 21 | + | | 22 | + // Test delete with member expression | | 23 | + r.complete( | | 24 | + 'delete globalThis._', | | 25 | + common.mustSucceed((completions) => { | | 26 | + assert.strictEqual(completions[1], 'globalThis._'); | | 27 | + | | 28 | + // Test delete with identifier | | 29 | + r.complete( | | 30 | + 'delete globalThis', | | 31 | + common.mustSucceed((completions) => { | | 32 | + assert.strictEqual(completions[1], 'globalThis'); | | 33 | + r.close(); | | 34 | + done(); | | 35 | + }) | | 36 | + ); | | 37 | + }) | | 38 | + ); | | 39 | + }); | | 40 | + | | 41 | + it('should handle typeof operator correctly', (t, done) => { | | 42 | + const r = repl.start({ | | 43 | + prompt: '', | | 44 | + input: process.stdin, | | 45 | + output: process.stdout, | | 46 | + terminal: false, | | 47 | + }); | | 48 | + | | 49 | + r.complete( | | 50 | + 'typeof globalThis', | | 51 | + common.mustSucceed((completions) => { | | 52 | + assert.strictEqual(completions[1], 'globalThis'); | | 53 | + r.close(); | | 54 | + done(); | | 55 | + }) | | 56 | + ); | | 57 | + }); | | 58 | + | | 59 | + it('should handle void operator correctly', (t, done) => { | | 60 | + const r = repl.start({ | | 61 | + prompt: '', | | 62 | + input: process.stdin, | | 63 | + output: process.stdout, | | 64 | + terminal: false, | | 65 | + }); | | 66 | + | | 67 | + r.complete( | | 68 | + 'void globalThis', | | 69 | + common.mustSucceed((completions) => { | | 70 | + assert.strictEqual(completions[1], 'globalThis'); | | 71 | + r.close(); | | 72 | + done(); | | 73 | + }) | | 74 | + ); | | 75 | + }); | | 76 | + | | 77 | + it('should handle other unary operators correctly', (t, done) => { | | 78 | + const r = repl.start({ | | 79 | + prompt: '', | | 80 | + input: process.stdin, | | 81 | + output: process.stdout, | | 82 | + terminal: false, | | 83 | + }); | | 84 | + | | 85 | + const unaryOperators = [ | | 86 | + '!globalThis', | | 87 | + '+globalThis', | | 88 | + '-globalThis', | | 89 | + '~globalThis', | | 90 | + ]; | | 91 | + | | 92 | + let testIndex = 0; | | 93 | + | | 94 | + function testNext() { | | 95 | + if (testIndex >= unaryOperators.length) { | | 96 | + r.close(); | | 97 | + done(); | | 98 | + return; | | 99 | + } | | 100 | + | | 101 | + const testCase = unaryOperators[testIndex++]; | | 102 | + r.complete( | | 103 | + testCase, | | 104 | + common.mustSucceed((completions) => { | | 105 | + assert.strictEqual(completions[1], 'globalThis'); | | 106 | + testNext(); | | 107 | + }) | | 108 | + ); | | 109 | + } | | 110 | + | | 111 | + testNext(); | | 112 | + }); | | 113 | + | | 114 | + it('should still evaluate globalThis correctly after unary expression completion', (t, done) => { | | 115 | + const r = repl.start({ | | 116 | + prompt: '', | | 117 | + input: process.stdin, | | 118 | + output: process.stdout, | | 119 | + terminal: false, | | 120 | + }); | | 121 | + | | 122 | + // First trigger completion with delete | | 123 | + r.complete( | | 124 | + 'delete globalThis._', | | 125 | + common.mustSucceed(() => { | | 126 | + // Then evaluate globalThis | | 127 | + r.eval( | | 128 | + 'globalThis', | | 129 | + r.context, | | 130 | + 'test.js', | | 131 | + common.mustSucceed((result) => { | | 132 | + assert.strictEqual(typeof result, 'object'); | | 133 | + assert.ok(result !== null); | | 134 | + r.close(); | | 135 | + done(); | | 136 | + }) | | 137 | + ); | | 138 | + }) | | 139 | + ); | | 140 | + }); | | 141 | +}); |
|