Skip to content

Commit 8b866ad

Browse files
authored
Merge pull request #373 from asger-semmle/jsx-factory-import
Approved by xiemaisi
2 parents 1509752 + 47f59b4 commit 8b866ad

7 files changed

Lines changed: 111 additions & 24 deletions

File tree

change-notes/1.19/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
| Unused variable, import, function or class | Fewer results | This rule now flags import statements with multiple unused imports once. |
3939
| 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. |
4040
| Whitespace contradicts operator precedence | Fewer false-positive results | This rule no longer flags operators with asymmetric whitespace. |
41+
| Unused import | Fewer false-positive results | This rule no longer flags imports used by the `transform-react-jsx` Babel plugin. |
4142

4243
## Changes to QL libraries
4344

javascript/ql/src/Declarations/UnusedVariable.ql

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,17 @@ predicate isPropertyFilter(UnusedLocal v) {
5656
predicate isReactImportForJSX(UnusedLocal v) {
5757
exists (ImportSpecifier is |
5858
is.getLocal() = v.getADeclaration() and
59-
exists (JSXNode jsx | jsx.getTopLevel() = is.getTopLevel()) |
60-
v.getName() = "React" or
61-
// also accept legacy `@jsx` pragmas
59+
exists (JSXNode jsx | jsx.getTopLevel() = is.getTopLevel())
60+
|
61+
v.getName() = "React"
62+
or
63+
// legacy `@jsx` pragmas
6264
exists (JSXPragma p | p.getTopLevel() = is.getTopLevel() | p.getDOMName() = v.getName())
65+
or
66+
// JSX pragma from a .babelrc file
67+
exists (Babel::TransformReactJsxConfig plugin |
68+
plugin.appliesTo(is.getTopLevel()) and
69+
plugin.getJsxFactoryVariableName() = v.getName())
6370
)
6471
}
6572

javascript/ql/src/semmle/javascript/frameworks/Babel.qll

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,62 @@ module Babel {
2828
result.(JSONArray).getElementStringValue(0) = pluginName
2929
)
3030
}
31+
32+
/**
33+
* Gets a file affected by this Babel configuration.
34+
*/
35+
Container getAContainerInScope() {
36+
result = getFile().getParentContainer()
37+
or
38+
result = getAContainerInScope().getAChildContainer() and
39+
// File-relative .babelrc search stops at any package.json or .babelrc file.
40+
not result.getAChildContainer() = any(PackageJSON pkg).getFile() and
41+
not result.getAChildContainer() = any(Config pkg).getFile()
42+
}
43+
44+
/**
45+
* Holds if this configuration applies to `tl`.
46+
*/
47+
predicate appliesTo(TopLevel tl) {
48+
tl.getFile() = getAContainerInScope()
49+
}
50+
}
51+
52+
/**
53+
* Configuration object for a Babel plugin.
54+
*/
55+
class Plugin extends JSONValue {
56+
Config cfg;
57+
string pluginName;
58+
59+
Plugin() {
60+
this = cfg.getPluginConfig(pluginName)
61+
}
62+
63+
/** Gets the name of the plugin being installed. */
64+
string getPluginName() {
65+
result = pluginName
66+
}
67+
68+
/** Gets the enclosing Babel configuration object. */
69+
Config getConfig() {
70+
result = cfg
71+
}
72+
73+
/** Gets the options value passed to the plugin, if any. */
74+
JSONValue getOptions() {
75+
result = this.(JSONArray).getElementValue(1)
76+
}
77+
78+
/** Gets a named option from the option object, if present. */
79+
JSONValue getOption(string name) {
80+
result = getOptions().(JSONObject).getPropValue(name)
81+
}
82+
83+
/** Holds if this plugin applies to `tl`. */
84+
predicate appliesTo(TopLevel tl) {
85+
cfg.appliesTo(tl)
86+
}
3187
}
3288

3389
/**
@@ -38,11 +94,9 @@ module Babel {
3894
* each path is of the form `{ "rootPathPrefix": "...", "rootPathSuffix": "..." }` and explicitly
3995
* specifies a mapping from a path prefix to a root.
4096
*/
41-
class RootImportConfig extends JSONArray {
42-
Config cfg;
43-
97+
class RootImportConfig extends Plugin {
4498
RootImportConfig() {
45-
this = cfg.getPluginConfig("babel-plugin-root-import")
99+
pluginName = "babel-plugin-root-import"
46100
}
47101

48102
/**
@@ -62,15 +116,16 @@ module Babel {
62116
*/
63117
private JSONObject getARootPathSpec() {
64118
// ["babel-plugin-root-import", <spec>]
65-
result = getElementValue(1) and
119+
result = getOptions() and
66120
exists(result.getPropValue("rootPathSuffix"))
67121
or
68122
exists (JSONArray pathSpecs |
69123
// ["babel-plugin-root-import", [ <spec>... ] ]
70-
pathSpecs = getElementValue(1)
124+
pathSpecs = getOptions()
71125
or
72126
// ["babel-plugin-root-import", { "paths": [ <spec> ... ] }]
73-
pathSpecs = getElementValue(1).(JSONObject).getPropValue("paths") |
127+
pathSpecs = getOption("paths")
128+
|
74129
result = pathSpecs.getElementValue(_)
75130
)
76131
}
@@ -95,37 +150,30 @@ module Babel {
95150
Folder getFolder() {
96151
result = getFile().getParentContainer()
97152
}
98-
99-
/**
100-
* Holds if this configuration applies to `tl`.
101-
*/
102-
predicate appliesTo(TopLevel tl) {
103-
tl.getFile().getParentContainer+() = getFolder()
104-
}
105153
}
106154

107155
/**
108156
* An import path expression that may be transformed by `babel-plugin-root-import`.
109157
*/
110158
private class BabelRootTransformedPathExpr extends PathExpr, Expr {
111-
RootImportConfig cfg;
159+
RootImportConfig plugin;
112160
string rawPath;
113161
string prefix;
114162
string mappedPrefix;
115163
string suffix;
116164

117165
BabelRootTransformedPathExpr() {
118166
this instanceof PathExpr and
119-
cfg.appliesTo(getTopLevel()) and
167+
plugin.appliesTo(getTopLevel()) and
120168
rawPath = getStringValue() and
121169
prefix = rawPath.regexpCapture("(.)/(.*)", 1) and
122170
suffix = rawPath.regexpCapture("(.)/(.*)", 2) and
123-
mappedPrefix = cfg.getRoot(prefix)
171+
mappedPrefix = plugin.getRoot(prefix)
124172
}
125173

126174
/** Gets the configuration that applies to this path. */
127-
RootImportConfig getConfig() {
128-
result = cfg
175+
RootImportConfig getPlugin() {
176+
result = plugin
129177
}
130178

131179
override string getValue() {
@@ -134,7 +182,7 @@ module Babel {
134182

135183
override Folder getSearchRoot(int priority) {
136184
priority = 0 and
137-
result = cfg.getFolder()
185+
result = plugin.getFolder()
138186
}
139187
}
140188

@@ -149,7 +197,24 @@ module Babel {
149197
}
150198

151199
override Folder getARootFolder() {
152-
result = pathExpr.getConfig().getFolder()
200+
result = pathExpr.getPlugin().getFolder()
201+
}
202+
}
203+
204+
/**
205+
* A configuration object for the `transform-react-jsx` plugin.
206+
*
207+
* The plugin option `{"pragma": xxx}` specifies a variable name used to instantiate
208+
* JSX elements.
209+
*/
210+
class TransformReactJsxConfig extends Plugin {
211+
TransformReactJsxConfig() {
212+
pluginName = "transform-react-jsx"
213+
}
214+
215+
/** Gets the name of the variable used to create JSX elements. */
216+
string getJsxFactoryVariableName() {
217+
result = getOption("pragma").(JSONString).getValue()
153218
}
154219
}
155220
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"plugins": [
3+
["transform-react-jsx", { "pragma": "h" }]
4+
]
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { h } from 'preact'; // OK - JSX element uses 'h' after babel compilation
2+
import { q } from 'preact'; // NOT OK - not used
3+
4+
export default (<div>Hello</div>);

javascript/ql/test/query-tests/Declarations/UnusedVariable/UnusedVariable.expected

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

0 commit comments

Comments
 (0)