UNPKG

chrome-devtools-frontend

Version:
725 lines (641 loc) • 31.4 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Workspace from '../../models/workspace/workspace.js'; import {createTarget, describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import {MockProtocolBackend, parseScopeChain} from '../../testing/MockScopeChain.js'; import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import * as TextEditor from '../../ui/components/text_editor/text_editor.js'; import * as Sources from './sources.js'; const {urlString} = Platform.DevToolsPath; describeWithMockConnection('Inline variable view scope helpers', () => { const URL = urlString`file:///tmp/example.js`; let target: SDK.Target.Target; let backend: MockProtocolBackend; beforeEach(() => { const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const targetManager = SDK.TargetManager.TargetManager.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); target = createTarget(); backend = new MockProtocolBackend(); }); async function toOffsetWithSourceMap( sourceMap: SDK.SourceMap.SourceMap|undefined, location: SDK.DebuggerModel.Location|null) { if (!location || !sourceMap) { return null; } const entry = sourceMap.findEntry(location.lineNumber, location.columnNumber); if (!entry || !entry.sourceURL) { return null; } const content = sourceMap.embeddedContentByURL(entry.sourceURL); if (!content) { return null; } const text = new TextUtils.Text.Text(content); return text.offsetFromPosition(entry.sourceLineNumber, entry.sourceColumnNumber); } async function toOffset(source: string|null, location: SDK.DebuggerModel.Location|null) { if (!location || !source) { return null; } const text = new TextUtils.Text.Text(source); return text.offsetFromPosition(location.lineNumber, location.columnNumber); } it('can resolve single scope mappings with source map', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This example was minified with terser v5.7.0 with following command. // 'terser index.js -m --toplevel -o example.min.js --source-map "url=example.min.js.map,includeSources"' const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; // The original scopes below have to match with how the source map translates the scope, so it // does not align perfectly with the source language scopes. In principle, this test could only // assert that the tests are approximately correct; currently, we assert an exact match. const originalSource = 'function unminified(par1, par2) {\n console.log(par1, par2);\n}\nunminified(1, 2);\n'; const originalScopes = ' { \n \n }'; const expectedOffsets = parseScopeChain(originalScopes); const sourceMapContent = { version: 3, names: ['unminified', 'par1', 'par2', 'console', 'log'], sources: ['index.js'], sourcesContent: [originalSource], mappings: 'AAAA,SAASA,EAAWC,EAAMC,GACxBC,QAAQC,IAAIH,EAAMC,EACpB,CACAF,EAAW,EAAG', }; const sourceMapJson = JSON.stringify(sourceMapContent); const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 42}, {name: 'n', value: 1}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapJson}, [scopeObject]); // Get source map for mapping locations to 'editor' offsets. const sourceMap = await callFrame.debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffsetWithSourceMap(sourceMap, l)); const text = new TextUtils.Text.Text(originalSource); assert.lengthOf(scopeMappings, 1); assert.strictEqual( scopeMappings[0].scopeStart, text.offsetFromPosition(expectedOffsets[0].startLine, expectedOffsets[0].startColumn)); assert.strictEqual( scopeMappings[0].scopeEnd, text.offsetFromPosition(expectedOffsets[0].endLine, expectedOffsets[0].endColumn)); assert.strictEqual(scopeMappings[0].variableMap.get('par1')?.value, 42); assert.strictEqual(scopeMappings[0].variableMap.get('par2')?.value, 1); }); it('can resolve nested scope mappings with source map', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This example was minified with terser v5.7.0 with following command. // 'terser index.js -m --toplevel -o example.min.js --source-map "url=example.min.js.map,includeSources"' const source = `function o(o){const n=console.log.bind(console);for(let c=0;c<o;c++)n(c)}o(10);\n//# sourceMappingURL=${ sourceMapUrl}`; const scopes = ' { < >} '; const originalSource = 'function f(n) {\n const c = console.log.bind(console);\n for (let i = 0; i < n; i++) c(i);\n}\nf(10);\n'; const originalScopes = ' { \n \n < >\n }'; const expectedOffsets = parseScopeChain(originalScopes); const sourceMapContent = { version: 3, names: ['f', 'n', 'c', 'console', 'log', 'bind', 'i'], sources: ['index.js'], sourcesContent: [originalSource], mappings: 'AAAA,SAASA,EAAEC,GACT,MAAMC,EAAIC,QAAQC,IAAIC,KAAKF,SAC3B,IAAK,IAAIG,EAAI,EAAGA,EAAIL,EAAGK,IAAKJ,EAAEI,EAChC,CACAN,EAAE', }; const sourceMapJson = JSON.stringify(sourceMapContent); const functionScopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 10}, {name: 'n', value: 1234}]); const forScopeObject = backend.createSimpleRemoteObject([{name: 'c', value: 5}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapJson}, [forScopeObject, functionScopeObject]); // Get source map for mapping locations to 'editor' offsets. const sourceMap = await callFrame.debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffsetWithSourceMap(sourceMap, l)); const text = new TextUtils.Text.Text(originalSource); assert.lengthOf(scopeMappings, 2); assert.strictEqual( scopeMappings[0].scopeStart, text.offsetFromPosition(expectedOffsets[0].startLine, expectedOffsets[0].startColumn)); assert.strictEqual( scopeMappings[0].scopeEnd, text.offsetFromPosition(expectedOffsets[0].endLine, expectedOffsets[0].endColumn)); assert.strictEqual(scopeMappings[0].variableMap.get('i')?.value, 5); assert.strictEqual(scopeMappings[0].variableMap.size, 1); assert.strictEqual( scopeMappings[1].scopeStart, text.offsetFromPosition(expectedOffsets[1].startLine, expectedOffsets[1].startColumn)); assert.strictEqual( scopeMappings[1].scopeEnd, text.offsetFromPosition(expectedOffsets[1].endLine, expectedOffsets[1].endColumn)); assert.strictEqual(scopeMappings[1].variableMap.get('n')?.value, 10); assert.strictEqual(scopeMappings[1].variableMap.get('c')?.value, 1234); assert.strictEqual(scopeMappings[1].variableMap.size, 2); }); it('can resolve simple scope mappings', async () => { const source = 'function f(a) { debugger } f(1)'; const scopes = ' { }'; const expectedOffsets = parseScopeChain(scopes); const functionScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); const callFrame = await backend.createCallFrame(target, {url: URL, content: source}, scopes, null, [functionScopeObject]); const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); assert.lengthOf(scopeMappings, 1); assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); assert.strictEqual(scopeMappings[0].variableMap.get('a')?.value, 1); assert.strictEqual(scopeMappings[0].variableMap.size, 1); }); it('can resolve nested scope mappings for block with no variables', async () => { const source = 'function f() { let a = 1; { debugger } } f()'; const scopes = ' { < > }'; const expectedOffsets = parseScopeChain(scopes); const functionScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); const blockScopeObject = backend.createSimpleRemoteObject([]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, null, [blockScopeObject, functionScopeObject]); const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); assert.lengthOf(scopeMappings, 2); assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); assert.strictEqual(scopeMappings[0].variableMap.size, 0); assert.strictEqual(scopeMappings[1].scopeStart, expectedOffsets[1].startColumn); assert.strictEqual(scopeMappings[1].scopeEnd, expectedOffsets[1].endColumn); assert.strictEqual(scopeMappings[1].variableMap.get('a')?.value, 1); assert.strictEqual(scopeMappings[1].variableMap.size, 1); }); it('can resolve nested scope mappings for function with no variables', async () => { const source = 'function f() { console.log("Hi"); { let a = 1; debugger } } f()'; const scopes = ' { < > }'; const expectedOffsets = parseScopeChain(scopes); const functionScopeObject = backend.createSimpleRemoteObject([]); const blockScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, null, [blockScopeObject, functionScopeObject]); const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); assert.lengthOf(scopeMappings, 2); assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); assert.strictEqual(scopeMappings[0].variableMap.size, 1); assert.strictEqual(scopeMappings[0].variableMap.get('a')?.value, 1); assert.strictEqual(scopeMappings[1].scopeStart, expectedOffsets[1].startColumn); assert.strictEqual(scopeMappings[1].scopeEnd, expectedOffsets[1].endColumn); assert.strictEqual(scopeMappings[1].variableMap.size, 0); }); }); function makeState(doc: string, extensions: CodeMirror.Extension = []) { return CodeMirror.EditorState.create({ doc, extensions: [ extensions, TextEditor.Config.baseConfiguration(doc), TextEditor.Config.autocompletion.instance(), ], }); } describeWithEnvironment('Inline variable view parser', () => { it('parses simple identifier', () => { const state = makeState('c', CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 0, 1, 1); assert.deepEqual(variables, [{line: 0, from: 0, id: 'c'}]); }); it('parses simple function', () => { const code = `function f(o) { let a = 1; debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}]); }); it('parses patterns', () => { const code = `function f(o) { let {x: a, y: [b, c]} = {x: o, y: [1, 2]}; console.log(a + b + c); debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [ {line: 0, from: 11, id: 'o'}, {line: 1, from: 30, id: 'a'}, {line: 1, from: 37, id: 'b'}, {line: 1, from: 40, id: 'c'}, {line: 1, from: 50, id: 'o'}, {line: 2, from: 71, id: 'console'}, {line: 2, from: 83, id: 'a'}, {line: 2, from: 87, id: 'b'}, {line: 2, from: 91, id: 'c'}, ]); }); it('parses function with nested block', () => { const code = `function f(o) { let a = 1; { let a = 2; debugger; } }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual( variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 3, from: 53, id: 'a'}]); }); it('parses function variable, ignores shadowing let in sibling block', () => { const code = `function f(o) { let a = 1; { let a = 2; console.log(a); } debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual( variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 68, id: 'console'}]); }); it('parses function variable, ignores shadowing const in sibling block', () => { const code = `function f(o) { let a = 1; { const a = 2; console.log(a); } debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual( variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 70, id: 'console'}]); }); it('parses function variable, ignores shadowing typed const in sibling block', () => { const code = `function f(o) { let a: number = 1; { const a: number = 2; console.log(a); } debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual( variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 86, id: 'console'}]); }); it('parses function variable, reports all vars', () => { const code = `function f(o) { var a = 1; { var a = 2; console.log(a); } debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [ {line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 3, from: 53, id: 'a'}, {line: 4, from: 68, id: 'console'}, {line: 4, from: 80, id: 'a'}, ]); }); it('parses function variable, handles shadowing in doubly nested scopes', () => { const code = `function f() { let a = 1; let b = 2; let c = 3; { let b; { const c = 4; b = 5; console.log(c); } console.log(c); } debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [ {line: 1, from: 25, id: 'a'}, {line: 2, from: 42, id: 'b'}, {line: 3, from: 59, id: 'c'}, {line: 9, from: 149, id: 'console'}, {line: 11, from: 183, id: 'console'}, {line: 11, from: 195, id: 'c'}, ]); }); it('parses function variable, handles shadowing with object pattern', () => { const code = `function f() { let a = 1; { let {x: b, y: a} = {x: 1, y: 2}; console.log(a + b); } console.log(a); debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [ {line: 1, from: 25, id: 'a'}, {line: 4, from: 89, id: 'console'}, {line: 6, from: 123, id: 'console'}, {line: 6, from: 135, id: 'a'}, ]); }); it('parses function variable, handles shadowing with array pattern', () => { const code = `function f() { let a = 1; { const [b, a] = [1, 2]; console.log(a + b); } console.log(a); debugger; }`; const state = makeState(code, CodeMirror.javascript.javascriptLanguage); const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); assert.deepEqual(variables, [ {line: 1, from: 25, id: 'a'}, {line: 4, from: 79, id: 'console'}, {line: 6, from: 113, id: 'console'}, {line: 6, from: 125, id: 'a'}, ]); }); }); describeWithEnvironment('Inline variable view scope value resolution', () => { it('resolves single variable in single scope', () => { const value42 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 42} as SDK.RemoteObject.RemoteObject; const scopeMappings = [{scopeStart: 0, scopeEnd: 10, variableMap: new Map([['a', value42]])}]; const variableNames = [{line: 3, from: 5, id: 'a'}]; const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); assert.strictEqual(valuesByLine?.size, 1); assert.strictEqual(valuesByLine?.get(3)?.size, 1); assert.strictEqual(valuesByLine?.get(3)?.get('a')?.value, 42); }); it('resolves shadowed variables', () => { const value1 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 1} as SDK.RemoteObject.RemoteObject; const value2 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 2} as SDK.RemoteObject.RemoteObject; const scopeMappings = [ {scopeStart: 10, scopeEnd: 20, variableMap: new Map([['a', value1]])}, {scopeStart: 0, scopeEnd: 30, variableMap: new Map([['a', value2]])}, ]; const variableNames = [ {line: 0, from: 5, id: 'a'}, // Falls into the outer scope. {line: 10, from: 15, id: 'a'}, // Inner scope. {line: 20, from: 25, id: 'a'}, // Outer scope. {line: 30, from: 35, id: 'a'}, // Outside of any scope. ]; const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); assert.strictEqual(valuesByLine?.size, 3); assert.strictEqual(valuesByLine?.get(0)?.size, 1); assert.strictEqual(valuesByLine?.get(0)?.get('a')?.value, 2); assert.strictEqual(valuesByLine?.get(10)?.size, 1); assert.strictEqual(valuesByLine?.get(10)?.get('a')?.value, 1); assert.strictEqual(valuesByLine?.get(20)?.size, 1); assert.strictEqual(valuesByLine?.get(20)?.get('a')?.value, 2); }); it('resolves multiple variables on the same line', () => { const value1 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 1} as SDK.RemoteObject.RemoteObject; const value2 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 2} as SDK.RemoteObject.RemoteObject; const scopeMappings = [{scopeStart: 10, scopeEnd: 20, variableMap: new Map([['a', value1], ['b', value2]])}]; const variableNames = [ {line: 10, from: 11, id: 'a'}, {line: 10, from: 13, id: 'b'}, {line: 10, from: 15, id: 'a'}, ]; const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); assert.strictEqual(valuesByLine?.size, 1); assert.strictEqual(valuesByLine?.get(10)?.size, 2); assert.strictEqual(valuesByLine?.get(10)?.get('a')?.value, 1); assert.strictEqual(valuesByLine?.get(10)?.get('b')?.value, 2); }); }); describe('DebuggerPlugin', () => { describe('computePopoverHighlightRange', () => { const {computePopoverHighlightRange} = Sources.DebuggerPlugin; it('correctly returns highlight range depending on cursor position and selection', () => { const doc = 'Hello World!'; const selection = CodeMirror.EditorSelection.create([ CodeMirror.EditorSelection.range(2, 5), ]); const state = CodeMirror.EditorState.create({doc, selection}); assert.isNull(computePopoverHighlightRange(state, 'text/plain', 0)); assert.deepInclude(computePopoverHighlightRange(state, 'text/plain', 2), {from: 2, to: 5}); assert.deepInclude(computePopoverHighlightRange(state, 'text/plain', 5), {from: 2, to: 5}); assert.isNull(computePopoverHighlightRange(state, 'text/plain', 10)); assert.isNull(computePopoverHighlightRange(state, 'text/plain', doc.length - 1)); }); describe('in JavaScript files', () => { const extensions = [CodeMirror.javascript.javascript()]; it('correctly returns highlight range for member assignments', () => { const doc = 'obj.foo = 42;'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 0), {from: 0, to: 3}); assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 4), {from: 0, to: 7}); }); it('correctly returns highlight range for member assignments involving `this`', () => { const doc = 'this.x = bar;'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 0), {from: 0, to: 4}); assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 5), {from: 0, to: 6}); }); it('correctly reports function calls as potentially side-effecting', () => { const doc = 'getRandomCoffee().name'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('getRandomCoffee')), {containsSideEffects: false}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.lastIndexOf('.')), {containsSideEffects: true}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('name')), {containsSideEffects: true}, ); }); it('correctly reports method calls as potentially side-effecting', () => { const doc = 'utils.getRandomCoffee().name'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('utils')), {containsSideEffects: false}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('getRandomCoffee')), {containsSideEffects: false}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.lastIndexOf('.')), {containsSideEffects: true}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('name')), {containsSideEffects: true}, ); }); it('correctly reports function calls in property accesses as potentially side-effecting', () => { const doc = 'bar[foo()]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('bar')), {containsSideEffects: false, from: 0, to: 'bar'.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correct reports postfix increments in property accesses as potentially side-effecting', () => { const doc = 'a[i++]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correctly reports postfix decrements in property accesses as potentially side-effecting', () => { const doc = 'a[i--]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correctly reports prefix increments in property accesses as potentially side-effecting', () => { const doc = 'array[++index]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correctly reports prefix decrements in property accesses as potentially side-effecting', () => { const doc = 'array[--index]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correctly reports assignment expressions in property accesses as potentially side-effecting', () => { const doc = 'array[index *= 5]'; const state = CodeMirror.EditorState.create({doc, extensions}); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), {containsSideEffects: true, from: 0, to: doc.length}, ); assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), {containsSideEffects: true, from: 0, to: doc.length}, ); }); it('correctly reports potential side-effects within a larger script', () => { const doc = `var a = new Array(); var i = 0; a[i++]; a[i--]; a[++i]; a[--i]; a[i *= 5]; a[foo()];`; const state = CodeMirror.EditorState.create({doc, extensions}); for (let offset = 0; (offset = doc.indexOf('a[', offset) + 1) !== 0;) { assert.deepInclude( computePopoverHighlightRange(state, 'text/javascript', offset), {containsSideEffects: true}, ); } }); }); describe('in HTML files', () => { it('correctly returns highlight range for variables in inline <script>s', () => { const doc = `<!DOCTYPE html> <script type="text/javascript"> globalThis.foo = bar + baz; </script>`; const extensions = [CodeMirror.html.html()]; const state = CodeMirror.EditorState.create({doc, extensions}); for (const name of ['bar', 'baz']) { const from = doc.indexOf(name); const to = from + name.length; assert.deepInclude( computePopoverHighlightRange(state, 'text/html', from), {from, to}, `did not correct highlight '${name}'`, ); } }); it('correctly returns highlight range for variables in inline event handlers', () => { const doc = `<!DOCTYPE html> <button onclick="foo(bar, baz)">Click me!</button>`; const extensions = [CodeMirror.html.html()]; const state = CodeMirror.EditorState.create({doc, extensions}); for (const name of ['foo', 'bar', 'baz']) { const from = doc.indexOf(name); const to = from + name.length; assert.deepInclude( computePopoverHighlightRange(state, 'text/html', from), {from, to}, `did not correct highlight '${name}'`, ); } }); }); describe('in TSX files', () => { it('correctly returns highlight range for field accesses', () => { const doc = `function foo(obj: any): number { return obj.x + obj.y; }`; const extensions = [CodeMirror.javascript.tsxLanguage]; const state = CodeMirror.EditorState.create({doc, extensions}); for (const name of ['x', 'y']) { const pos = doc.lastIndexOf(name); const from = pos - 4; const to = pos + name.length; assert.deepInclude( computePopoverHighlightRange(state, 'text/typescript-jsx', pos), {from, to}, `did not correct highlight '${name}'`, ); } }); }); }); });