-
Notifications
You must be signed in to change notification settings - Fork 750
[Preview] mis-judge in scope inner locations #4699
[Preview] mis-judge in scope inner locations #4699
Conversation
|
Waiting the test running... |
a2ad00d to
fba6467
Compare
|
@jasonLaster [1] https://github.com/devtools-html/debugger.html/blob/master/docs/local-development.md#testing |
|
@Fischer-L I don't think including inner scopes of the current scope is the right approach here. By doing so, we've brought the inner function body into scope when it isn't. I'd prefer to convert our out of scope calculation to respect column and line rather than just line. We're already receiving this information from the parser but it's been simplified to lines by the {{linesInScope}} selector. @jasonLaster - Thoughts? |
|
@ryanjduffy i agree that it would be nicer to out of scope lines & columns. I have not looked at this solution though :) so can't speak to what is going on here |
OK, this leads questions to discuss:
To allow developers to peek into these closure variables is like providing a convenient shortcut during debugging. Yes, we don't necessarily go like other browsers and could be strict on this but just would lose some friendly ability for developers. function doA(obj) {
let result = obj.array && obj.array.map( v => { // bp
// convenient to be able to preview `a_closure_var_a_bit_away` on mouse over
return v + a_closure_var_a_bit_away.base;
});
return result
}
function doB(a, b) { // Line 10
let v = a > 0 ? a : nums.find(v => (v <= b && // Line 11
v >= 0 && v % 2 == 0)); // Line 12
return v * 2; // Line 13, bp
} // Line 14
|
|
Thanks for the write up! Great points, and i agree that hovering on an inner scope should work. Even though it technically is out of scope...
Fortunately pause data has the currently line & column. I suggest looking at the selected frame because this has to work regardless of which frame is selected. Thanks for the hard work here, it's a super interesting problem! |
|
Is the line/column discussion a red herring here, at least partially? The question seems to be "Is the hovered token in scope?" rather than "Is the location in scope?". To your point:
It make sense to be able to preview a variable that is in the current scope even if it resides in a different scope. So, in the following example, if I pause on |
4f9d2b3 to
9689ebc
Compare
| } | ||
|
|
||
| const contains = | ||
| locations.filter(a => containsLocation(a, location)).length > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is O(n^2) complexity.
Consider there are 10 locations to test: [ L1, L2, ..., L10 ] and only L7 contains L8 ~ L10.
Loop 1: we get [ L1 ]
Loop 2: we test L2 against [ L1 ]
Loop 3: we test L3 against [ L1, L2 ]
... ...
Loop 8: we test L8 against [ L1, .., L7 ]
Loop 9: we test L9 against [ L1, .., L7 ]
Loop 10: we test L10 against [ L1, .., L7 ]
| .sort(sortByStart); | ||
| .sort(sortByStart) | ||
| .filter(loc => !containsPosition(loc, position)); | ||
| return removeOverlaps(locations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sort should be O(nlogn). The old removeOverlaps is O(n^2) so the old way was doing O(n^2) then O(nlogn).
Try to improve that, just do once O(nlogn) then the following operations can do O(n)
| (12, 22) -> (14, 3) | ||
| (16, 16) -> (18, 3) | ||
| (20, 27) -> (22, 3) | ||
| (24, 9) -> (26, 3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are in fact the inner locations under the location enclosing the bp so shouldn't be out of scope
src/workers/parser/utils/contains.js
Outdated
| import type { Node } from "babel-traverse"; | ||
|
|
||
| function startsBefore(a: AstLocation, b: AstPosition) { | ||
| export function startsBefore(a: AstLocation, b: AstPosition) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reuse the function we did in PR #4381 we can do some optimization in removeInnerLocations
| } | ||
|
|
||
| const contains = | ||
| locations.filter(a => containsLocation(a, location)).length > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is O(n^2) complexity.
Consider there are 10 locations to test: [ L1, L2, ..., L10 ] and only L7 contains L8 ~ L10.
Loop 1: we get [ L1 ]
Loop 2: we test L2 against [ L1 ]
Loop 3: we test L3 against [ L1, L2 ]
... ...
Loop 8: we test L8 against [ L1, .., L7 ]
Loop 9: we test L9 against [ L1, .., L7 ]
Loop 10: we test L10 against [ L1, .., L7 ]
|
@jasonLaster Would you have a look the patch, thanks In the below test, I also try to step into the inner location. We can see the outer More to discuss:
|
| locations = removeInnerLocations(locations, position).filter( | ||
| loc => !containsPosition(loc, position) | ||
| ); | ||
| return removeOverlaps(locations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sort should be O(nlogn). The old removeOverlaps is O(n^2) so the old way was doing O(n^2) then O(nlogn).
Try to improve that, just do once O(nlogn) then the following operations can do O(n)
|
So... this is a bit hard to follow, looking at our test coverage and the fixture: https://github.com/Fischer-L/debugger.html/blob/4459-mis-judge-in-scope-inner-locations/src/workers/parser/tests/fixtures/outOfScope.js#L1 I wonder if it would help to have new tests that help cover these cases |
|
I think it is a bit strange to talk about |
| function removeInnerLocations(locations: AstLocation[], position: AstPosition) { | ||
| // First, let's find the nearest bp-enclosing function location, | ||
| // which is to loop locations to find the last location enclosing the bp. | ||
| let nearestPos = -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can replace this logic w/ a lodash helper:
this attempt is likely flawed, but you should see what I have in mind :P
const firstInnerLocation = findIndex(locations, loc => containsPosition(loc, position)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasonLaster I was trying to import and utilize lodash. It works fine on the local page but while running the mochi test it fails because
JavaScript error: , line 0: uncaught exception: TypeError: _lodash is undefined
JS Error:
"uncaught exception: TypeError: _lodash is undefined"]
Tried to search "lodash" under the mochi test folder but found nothing to set. Did I miss some settings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah :) that's tricky. I forgot that this is running in a worker
so the typical way to use lodash is import {x} from "lodash" in the worker you need to do import x from "lodash/x" because we can't require lodash at runtime in the worker...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, now with a lodash findLastIndex
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we replace this with a lodash helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now with a lodash findIndex
| locations = removeInnerLocations(locations, position).filter( | ||
| loc => !containsPosition(loc, position) | ||
| ); | ||
| return removeOverlaps(locations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
9689ebc to
28844c4
Compare
Originally wanted to reuse the
Removed the bp wording in comments p.s: during the local mochi test, saw the below error. At the 1st look this shouldn't relates to us here, wanna see the result on the server |
28844c4 to
42ec620
Compare
|
@jasonLaster The test looks fine. Would you look at the patch, thanks? |
jasonLaster
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great. I’m being cautious because i think it’s important to make this code as readable as possible.
Ideally, any one interested can visualize our algorithms and use it in their own projects :)
basically trying to make it so good it stands on its own merits and could be its own open source project!!!
| newLocs.push(loc); | ||
| parentIdx = i; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use findIndex here too?
| (29, 16) -> (33, 1) | ||
| (35, 20) -> (37, 1) | ||
| (39, 26) -> (41, 1) | ||
| (43, 20) -> (45, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a crazy idea, but i think it would be fun to write a helper function for our tests so that we can visualize what is in scope and what is out of scope.
for instance, we could write expect(highlightInScopeLocations(..)).toMatchSnapshot() and the locations in the code that are inscope are bolded with https://github.com/chalk/chalk
This would make it really fun to debug this code because you could quickly visualize what the algorithm outputs!
function parentFunc() {
let MAX = 3;
let nums = [0, 1, 2, 3];
let x = 1;
let y = nums.find(function(n) {
return n == x;
});
function innerFunc(a) {
return Math.max(a, MAX);
}
return innerFunc(y);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean printing out in-scope function in tests/fixtures/outOfScope.js like this?
// This parentFunc is in-scope so from here codes are in green
function parentFunc() {
let MAX = 3;
let nums = [0, 1, 2, 3];
let x = 1;
let y = nums.find(function(n) {
return n == x;
});
function innerFunc(a) {
return Math.max(a, MAX);
}
return innerFunc(y);
}
// to here codes are in green
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, i might hack on this now that it has landed :)
| nearestPos + 1 | ||
| ); | ||
| lastInnerPos = | ||
| firstNotContained < 0 ? locations.length - 1 : firstNotContained - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets move this to a helper function... that returns lastInnerPos
| // which is to loop locations to find the last location enclosing the bp. | ||
| let nearestPos = findLastIndex(locations, loc => | ||
| containsPosition(loc, position) | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets move this out to a helper function too with the comments above the function
| const newLocs = []; | ||
| const length = locations.length; | ||
| if (length) { | ||
| newLocs.push(locations[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is easier to read as a reduce:
function deduplicateNode(nodes, location) {
const parent = nodes[nodes.length - 1];
if (containsLocation(parent, location)) {
return [ ...nodes, location ];
}
return nodes;
}
function removeOverlappingNodes(locations: AstLocation[]) {
const firstParent = locations[0];
return locations.reduce(deduplicateNode, [ firstParent ])
}| if (!contains) { | ||
| locations.push(location); | ||
| // Third, remove those inner functions | ||
| const newLocs = locations.slice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does locations need to be duplicated?
codehag
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some comments to make it easier to read, let me know if you need any help or have questions
| let lastInnerPos = -1; | ||
| if (nearestPos >= 0) { | ||
| let parentLoc = locations[nearestPos]; | ||
| let firstNotContained = findIndex( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the naming here is confusing. maybe something like outerBoundaryIndex?
| function removeInnerLocations(locations: AstLocation[], position: AstPosition) { | ||
| // First, let's find the nearest position-enclosing function location, | ||
| // which is to loop locations to find the last location enclosing the bp. | ||
| let nearestPos = findLastIndex(locations, loc => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this naming is a bit confusing, how about parentIndex or outerStartIndex (because nearest can also be adjacent, and Pos suggests Position similar to the ASTPosition, but this is an index. more specifically it is a boundary)
also we use it twice with +1; it makes sense to have that as a variable, maybe
const outerStartIndex = findLastIndex(locations, loc => containsPosition(loc, position));
const innerStartIndex = outerStartIndex + 1;| // once seeing the 1st location not enclosed by the nearest location | ||
| // to find the last inner locations inside the nearest location. | ||
| let lastInnerPos = -1; | ||
| if (nearestPos >= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes doing a positive check is making the code harder to read and adds unnecessary complexity. For this reason we usually prefer doing negative checks, because then we can return early. it also saves some work, in this case the memory allocation that is happening on 72 and also the other checks. You also wouldnt need to reassign lastInnerPos which is making this a bit more confusing to read.
Usually you want to use if statements sparingly. they force a reader to remember things, which is always extra mental load.
So maybe something like this would be a good idea:
if (nearestPos < 0) {
return locations;
}| loc => !containsLocation(parentLoc, loc), | ||
| nearestPos + 1 | ||
| ); | ||
| lastInnerPos = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this isnt a position but an index. maybe innerBoundaryIndex is clearer?
| locations.push(location); | ||
| // Third, remove those inner functions | ||
| const newLocs = locations.slice(); | ||
| if (nearestPos >= 0 && lastInnerPos >= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i dont think it is possible for lastInnerPos to be -1, since you have a check on line 68
42ec620 to
238b568
Compare
| function removeInnerLocations(locations: AstLocation[], position: AstPosition) { | ||
| // First, let's find the nearest position-enclosing function location, | ||
| // which is to loop locations to find the last location enclosing the position. | ||
| const newLocs = locations.slice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason returning new locations is because:
- Both
removeOverlapsand Arrayfilterreturn new locations. SinceremoveInnerLocationsalso does removal operation, that is for consistency, not to bring surprise. - Although this is not an exported function, for the removal operation probably less surprise without mutating the input arg. This is not necessary, however, consider the reason 1, consistency is good for less error.
| * so that we can do linear time complexity operation. | ||
| */ | ||
| function removeOverlaps(locations: AstLocation[]) { | ||
| if (locations.length == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important to take care of an empty array. Otherwise
// firstParent will be undefined if locations are empty
const firstParent = locations[0]; 238b568 to
388fc26
Compare
388fc26 to
c04a93a
Compare
| function removeInnerLocations(locations: AstLocation[], position: AstPosition) { | ||
| // First, let's find the nearest position-enclosing function location, | ||
| // which is to find the last location enclosing the position. | ||
| const newLocs = locations.slice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reasons returning new locations are because:
- Both
removeOverlapsand Arrayfilterreturn new locations. SinceremoveInnerLocationsalso does removal operation, that is for consistency, not to bring surprise. - Although this is not an exported function, for the removal operation probably less surprise without mutating the input arg. This is not necessary, however, consider the reason 1, consistency is good for less error.
|
@codehag Updated per comments, thanks |
codehag
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good, thanks!



Associated Issue: #4459
Screenshots/Videos