Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code lens for quick evaluation #1035

Merged
merged 8 commits into from
Dec 10, 2021

Conversation

aeisenberg
Copy link
Contributor

@aeisenberg aeisenberg commented Dec 6, 2021

Replace this with a description of the changes your pull request makes. Closes #446

Checklist

  • CHANGELOG.md has been updated to incorporate all user visible changes made by this pull request.
  • Issues have been created for any UI or other user-facing changes made by this pull request.
  • @github/docs-content-codeql has been cc'd in all issues for UI or other user-facing changes made by this pull request.

Copy link
Contributor

@adityasharad adityasharad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good start, and nice to see what we can achieve just from the editor.

Range
} from 'vscode';

class MyCodeLensProvider implements CodeLensProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give this class a meaningful name, e.g. QuickEvalCodeLensProvider. The file name can be codeLensProvider.ts to remain consistent with surrounding files.

const startIndex = textLine.text.search(regex);
const range: Range = new Range(
textLine.range.start.line, startIndex,
textLine.range.end.line, startIndex - 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negative moves left, positive moves right.

Suggested change
textLine.range.end.line, startIndex - 1
textLine.range.end.line, startIndex + 1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of something else when I made this change, but I didn't need to. Not sure now what that thought was. Changing back now. :)

const textLine = document.lineAt(index);
const regex = new RegExp(/(?=(\w+\(\s*.*(?:,\s*)*\)\s*\{))\1/g);
if (textLine.text.match(regex)) {
const uri = document.uri;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor cleanup: just use document.uri directly in the list of arguments.

for (let index = 0; index < document.lineCount; index++) {
const textLine = document.lineAt(index);
const regex = new RegExp(/(?=(\w+\(\s*.*(?:,\s*)*\)\s*\{))\1/g);
if (textLine.text.match(regex)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're regex matching twice: once with match and once with search. That can be avoided by just calling const startIndex = textLine.text.search(regex). If there is no match this will be -1, if there is a match this will be the start index of the matching substring.


for (let index = 0; index < document.lineCount; index++) {
const textLine = document.lineAt(index);
const regex = new RegExp(/(?=(\w+\(\s*.*(?:,\s*)*\)\s*\{))\1/g);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this regex performs lookahead to find a pattern without capturing it, and then uses \1 to capture that same pattern. I think you can simplify that. Try something like this.

It's also a good idea to include a human-readable comment every time you write a regex, either explaining the intent or giving a short example. Reading them is hard 😄

Suggested change
const regex = new RegExp(/(?=(\w+\(\s*.*(?:,\s*)*\)\s*\{))\1/g);
// Match a predicate signature, including the predicate name, parameter list, and the opening brace.
const regex = new RegExp(/\w+\([^()]*\)\s*\{/);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this change. I've always struggled writing regex (although I'm getting more comfortable with it now), so I always appreciate feedback. The only reason I included the positive lookahead was I was getting a code scanning failure about potential exponential backtracking. But I have also since simplified the regex, so maybe that won't be an issue this time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup I think your new regex is free of this problem, because of the simpler handling of the (...) parameters!

If you'd like to learn more about exponential regexes, the CodeQL docs for the alert you saw are pretty good: https://codeql.github.com/codeql-query-help/javascript/js-redos/. Exponential backtracking usually happens if you have something like (pattern1|pattern2)*, because at each step of matching the next character in the * sequence, we don't know whether we just saw a pattern1 or a pattern2, and have to backtrack to try both options if the next match fails.

extensions/ql-vscode/src/CodeLensProvider.ts Outdated Show resolved Hide resolved
extensions/ql-vscode/src/extension.ts Outdated Show resolved Hide resolved
extensions/ql-vscode/src/extension.ts Outdated Show resolved Hide resolved
@aeisenberg
Copy link
Contributor Author

Neat. I tried this out and it works. Though, it shows a quick eval suggestion even when the code is commented.

eg- this still shows a quick eval option:

// query BlockStmt foo(BlockStmt b) {
//   result = b and b.getNumStmt() >= 1
// }

@aeisenberg
Copy link
Contributor Author

And the following code does not show a quick eval option (notice the space before the (:

query BlockStmt foo (BlockStmt b) {
  result = b and b.getNumStmt() >= 1
}

This is an inherent limitation of using regexes. I think it's important to fix the first problem I mentioned, but this one is less important.

for (let index = 0; index < document.lineCount; index++) {
const textLine = document.lineAt(index);
// Match a predicate signature, including predicate name, parameter list, and opening brace.
const regex = new RegExp(/\w+\(\s*.*(?:,\s*)*\)\s*\{/);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice to get the name of the predicate in the codelens text. You can do that by capturing the word as below:

Suggested change
const regex = new RegExp(/\w+\(\s*.*(?:,\s*)*\)\s*\{/);
const regex = new RegExp(/(\w+)\(\s*.*(?:,\s*)*\)\s*\{/);

Then you can call: const matches = textLine.text.match(regex);

matches[1] contains the matched word.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made these changes. I also added an additional whitespace match between the word match and opening '(' to account for any predicate definition that has that space.


const command: Command = {
command: 'codeQL.codeLensQuickEval',
title: 'CodeQL: Quick Evaluation',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: 'CodeQL: Quick Evaluation',
title: `Quick Evaluation: ${matches[1]}`,

Copy link
Contributor

@adityasharad adityasharad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work wiring this together. A few follow up suggestions.

Comment on lines 21 to 23
const startIndex = textLine.text.search(regex);

if (startIndex !== -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid calling both match and search, because you're doubling the computation cost at each line by regex matching twice. Now that we're using information from the result of match, stick with that.
matches.index will give you the start index you need, and if (matches) will give you the condition to check whether the match succeeded (because it returns null if there is no match.

Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I got it fixed now. :)

async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this can ever be undefined, so might as well make the type strict.

Comment on lines 162 to 164
const codelensProvider = new QuickEvalCodeLensProvider();

languages.registerCodeLensProvider({ scheme: 'file', language: 'ql' }, codelensProvider);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this further down in the function: I don't think code lens creation should happen before we call languageSupport.install(), and certainly not before we've set up the extension and logging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to be after languageSupport.install(), but if you think it should go further down in the function I can move it again.

@@ -2,6 +2,8 @@

## [UNRELEASED]

- Add CodeLens feature to make the CodeQL quick evaluation command more accessible. [#1035](https://github.com/github/vscode-codeql/pull/1035)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've attempted to word this in a way that tells the user how to use the feature. What do you think?

Suggested change
- Add CodeLens feature to make the CodeQL quick evaluation command more accessible. [#1035](https://github.com/github/vscode-codeql/pull/1035)
- Add a CodeLens to make the Quick Evaluation command more accessible. Click the `Quick Evaluation` prompt above a predicate definition in the editor to evaluate that predicate on its own. [#1035](https://github.com/github/vscode-codeql/pull/1035)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. I tried to make it concise, but I should have included more information.

Copy link
Contributor Author

@aeisenberg aeisenberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking really good. Just a couple of more small cleanups.

Comment on lines 10 to 11
// Each provider requires a provideCodeLenses function which will give the various documents
// the code lenses
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can delete this boilerplate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this 👍🏻


const command: Command = {
command: 'codeQL.codeLensQuickEval',
title: `Quick Evaluation: ${matches![1]}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type assertion no longer necessary.

Suggested change
title: `Quick Evaluation: ${matches![1]}`,
title: `Quick Evaluation: ${matches[1]}`,

Copy link
Contributor

@marcnjaramillo marcnjaramillo Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change has been made. 👍🏻

const regex = new RegExp(/(\w+)\s*\(\s*.*(?:,\s*)*\)\s*\{/);
const matches = textLine.text.match(regex);

if (matches) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that commented code removes the code lens. It's too tricky to do in a single regex, so I think doing another check is ok.

Suggested change
if (matches) {
if (matches && textLine.text.search(/^\s*\//)) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had an illuminating discussion offline about why this works (search returns 0 when it matches the start of the line and -1 on mismatch, which JS converts into false and true respectively). I suggested a slightly more verbose but arguably simpler use of test instead -- hopefully that's still captured the meaning you suggested here!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A downside with the suggestion is that the regex is evaluated twice for every line, even if there is no initial match. The original suggestion only ran the second regex if the first one matched. I'm not sure if the extra overhead would be noticeable, but this code is run after every file change (in a background thread).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. We can keep using !test rather than search, but keep it inline instead of in its own variable.
So if (matches && /.../.test(textLine.text)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep....that makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change. Sorry it took a bit. I forgot I had resolved the CHANGELOG conflict from here but I hadn't updated my local branch yet. Had to get that squared away. Does it look better?

Copy link
Contributor Author

@aeisenberg aeisenberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but I can't approve since I was the one who created the PR.

@aeisenberg aeisenberg enabled auto-merge (rebase) December 9, 2021 23:21
@aeisenberg
Copy link
Contributor Author

@adityasharad Can you approve? I can't since I created this PR.

auto-merge was automatically disabled December 10, 2021 00:48

Head branch was pushed to by a user without write access

Copy link
Contributor Author

@aeisenberg aeisenberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

(I can't approve.)

@adityasharad adityasharad changed the title Codelens quick eval Add code lens for quick evaluation Dec 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a CodeLens for allowing users to run quick eval
3 participants