Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions change-notes/1.19/analysis-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
| Unused variable, import, function or class | Fewer results | This rule now flags import statements with multiple unused imports once. |
| User-controlled bypass of security check | Fewer results | This rule no longer flags conditions that guard early returns. The precision of this rule has been revised to "medium". Results are no longer shown on LGTM by default. |
| Whitespace contradicts operator precedence | Fewer false-positive results | This rule no longer flags operators with asymmetric whitespace. |
| Unused import | Fewer false-positive results | This rule no longer flags imports used by the `transform-react-jsx` Babel plugin. |

## Changes to QL libraries

Expand Down
13 changes: 10 additions & 3 deletions javascript/ql/src/Declarations/UnusedVariable.ql
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,17 @@ predicate isPropertyFilter(UnusedLocal v) {
predicate isReactImportForJSX(UnusedLocal v) {
exists (ImportSpecifier is |
is.getLocal() = v.getADeclaration() and
exists (JSXNode jsx | jsx.getTopLevel() = is.getTopLevel()) |
v.getName() = "React" or
// also accept legacy `@jsx` pragmas
exists (JSXNode jsx | jsx.getTopLevel() = is.getTopLevel())
|
v.getName() = "React"
or
// legacy `@jsx` pragmas
exists (JSXPragma p | p.getTopLevel() = is.getTopLevel() | p.getDOMName() = v.getName())
or
// JSX pragma from a .babelrc file
exists (Babel::TransformReactJsxConfig plugin |
plugin.appliesTo(is.getTopLevel()) and
plugin.getJsxFactoryVariableName() = v.getName())
)
}

Expand Down
107 changes: 86 additions & 21 deletions javascript/ql/src/semmle/javascript/frameworks/Babel.qll
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,62 @@ module Babel {
result.(JSONArray).getElementStringValue(0) = pluginName
)
}

/**
* Gets a file affected by this Babel configuration.
*/
Container getAContainerInScope() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We already have an appliesTo predicate in a subclass of Config; maybe we should just PUT that predicate here and improve it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hadn't noticed that, though it doesn't appear in a subclass, it appears in the other plugin class.

I've introduced a Plugin class to factor out some common logic in the two plugin classes. We now also have appliesTo on both Config and Plugin.

The getAContainerInScope() has to be there in some form or other, since the recursion steps through folders, which aren't top-levels. We can rename it if you like. I'd like to keep it public, though, so we can easily tweak it at customers.

result = getFile().getParentContainer()
or
result = getAContainerInScope().getAChildContainer() and
// File-relative .babelrc search stops at any package.json or .babelrc file.
not result.getAChildContainer() = any(PackageJSON pkg).getFile() and
not result.getAChildContainer() = any(Config pkg).getFile()
}

/**
* Holds if this configuration applies to `tl`.
*/
predicate appliesTo(TopLevel tl) {
tl.getFile() = getAContainerInScope()
}
}

/**
* Configuration object for a Babel plugin.
*/
class Plugin extends JSONValue {
Config cfg;
string pluginName;

Plugin() {
this = cfg.getPluginConfig(pluginName)
}

/** Gets the name of the plugin being installed. */
string getPluginName() {
result = pluginName
}

/** Gets the enclosing Babel configuration object. */
Config getConfig() {
result = cfg
}

/** Gets the options value passed to the plugin, if any. */
JSONValue getOptions() {
result = this.(JSONArray).getElementValue(1)
}

/** Gets a named option from the option object, if present. */
JSONValue getOption(string name) {
result = getOptions().(JSONObject).getPropValue(name)
}

/** Holds if this plugin applies to `tl`. */
predicate appliesTo(TopLevel tl) {
cfg.appliesTo(tl)
}
}

/**
Expand All @@ -38,11 +94,9 @@ module Babel {
* each path is of the form `{ "rootPathPrefix": "...", "rootPathSuffix": "..." }` and explicitly
* specifies a mapping from a path prefix to a root.
*/
class RootImportConfig extends JSONArray {
Config cfg;

class RootImportConfig extends Plugin {
RootImportConfig() {
this = cfg.getPluginConfig("babel-plugin-root-import")
pluginName = "babel-plugin-root-import"
}

/**
Expand All @@ -62,15 +116,16 @@ module Babel {
*/
private JSONObject getARootPathSpec() {
// ["babel-plugin-root-import", <spec>]
result = getElementValue(1) and
result = getOptions() and
exists(result.getPropValue("rootPathSuffix"))
or
exists (JSONArray pathSpecs |
// ["babel-plugin-root-import", [ <spec>... ] ]
pathSpecs = getElementValue(1)
pathSpecs = getOptions()
or
// ["babel-plugin-root-import", { "paths": [ <spec> ... ] }]
pathSpecs = getElementValue(1).(JSONObject).getPropValue("paths") |
pathSpecs = getOption("paths")
|
result = pathSpecs.getElementValue(_)
)
}
Expand All @@ -95,37 +150,30 @@ module Babel {
Folder getFolder() {
result = getFile().getParentContainer()
}

/**
* Holds if this configuration applies to `tl`.
*/
predicate appliesTo(TopLevel tl) {
tl.getFile().getParentContainer+() = getFolder()
}
}

/**
* An import path expression that may be transformed by `babel-plugin-root-import`.
*/
private class BabelRootTransformedPathExpr extends PathExpr, Expr {
RootImportConfig cfg;
RootImportConfig plugin;
string rawPath;
string prefix;
string mappedPrefix;
string suffix;

BabelRootTransformedPathExpr() {
this instanceof PathExpr and
cfg.appliesTo(getTopLevel()) and
plugin.appliesTo(getTopLevel()) and
rawPath = getStringValue() and
prefix = rawPath.regexpCapture("(.)/(.*)", 1) and
suffix = rawPath.regexpCapture("(.)/(.*)", 2) and
mappedPrefix = cfg.getRoot(prefix)
mappedPrefix = plugin.getRoot(prefix)
}

/** Gets the configuration that applies to this path. */
RootImportConfig getConfig() {
result = cfg
RootImportConfig getPlugin() {
result = plugin
}

override string getValue() {
Expand All @@ -134,7 +182,7 @@ module Babel {

override Folder getSearchRoot(int priority) {
priority = 0 and
result = cfg.getFolder()
result = plugin.getFolder()
}
}

Expand All @@ -149,7 +197,24 @@ module Babel {
}

override Folder getARootFolder() {
result = pathExpr.getConfig().getFolder()
result = pathExpr.getPlugin().getFolder()
}
}

/**
* A configuration object for the `transform-react-jsx` plugin.
*
* The plugin option `{"pragma": xxx}` specifies a variable name used to instantiate
* JSX elements.
*/
class TransformReactJsxConfig extends Plugin {
TransformReactJsxConfig() {
pluginName = "transform-react-jsx"
}

/** Gets the name of the variable used to create JSX elements. */
string getJsxFactoryVariableName() {
result = getOption("pragma").(JSONString).getValue()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": [
["transform-react-jsx", { "pragma": "h" }]
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { h } from 'preact'; // OK - JSX element uses 'h' after babel compilation
import { q } from 'preact'; // NOT OK - not used

export default (<div>Hello</div>);
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
| Babelrc/importPragma.jsx:2:1:2:27 | import ... react'; | Unused import q. |
| decorated.ts:1:1:1:126 | import ... where'; | Unused import actionHandler. |
| decorated.ts:4:10:4:12 | fun | Unused function fun. |
| externs.js:6:5:6:13 | iAmUnused | Unused variable iAmUnused. |
| importWithoutPragma.jsx:1:1:1:27 | import ... react'; | Unused import h. |
| multi-imports.js:1:1:1:29 | import ... om 'x'; | Unused imports a, b, d. |
| multi-imports.js:2:1:2:42 | import ... om 'x'; | Unused imports alphabetically, ordered. |
| typeoftype.ts:9:7:9:7 | y | Unused variable y. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { h } from 'preact'; // NOT OK - not in scope of .babelrc file

export default (<div>Hello</div>);