Skip to content

Commit 293888a

Browse files
committed
chore(no-release): support conditionals in #if
1 parent 1294543 commit 293888a

File tree

6 files changed

+215
-14
lines changed

6 files changed

+215
-14
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode
2+
node_modules/
3+

lib/parse-utils.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const parser = require("acorn");
2+
3+
/**
4+
* @param {string} text Code to evaluate
5+
* @returns {boolean | undefined} The result of the evaluation, `undefined` if
6+
* parsing failed or the result is unknown.
7+
*/
8+
function evaluate(text) {
9+
try {
10+
const ast = parser.parse(text, { ecmaVersion: 2020 });
11+
12+
const expressionStatement = ast.body[0];
13+
14+
if (expressionStatement.type !== "ExpressionStatement") {
15+
throw new Error("Expected an ExpressionStatement");
16+
}
17+
18+
const result = visitExpression(expressionStatement.expression);
19+
20+
return result;
21+
} catch {
22+
// Return an unknown result if parsing failed
23+
return undefined;
24+
}
25+
}
26+
27+
/**
28+
* @param {import("acorn").Expression} node
29+
*/
30+
const visitExpression = (node) => {
31+
if (node.type === "LogicalExpression") {
32+
return visitLogicalExpression(node);
33+
} else if (node.type === "UnaryExpression") {
34+
return visitUnaryExpression(node);
35+
} else if (node.type === "CallExpression") {
36+
return visitCallExpression(node);
37+
} else {
38+
throw new Error(`Unknown node type: ${node.type} ${JSON.stringify(node)}`);
39+
}
40+
}
41+
42+
/**
43+
* @param {import("acorn").LogicalExpression} node
44+
*/
45+
const visitLogicalExpression = (node) => {
46+
const left = visitExpression(node.left);
47+
const right = visitExpression(node.right);
48+
49+
if (node.operator === "&&") {
50+
// We can shortcircuit regardless of `unknown` if either are false.
51+
if (left === false || right === false) {
52+
return false;
53+
} else if (left === undefined || right === undefined) {
54+
return undefined;
55+
} else {
56+
return left && right;
57+
}
58+
} else if (node.operator === "||") {
59+
if (left === undefined || right === undefined) {
60+
return undefined;
61+
} else {
62+
return left || right;
63+
}
64+
}
65+
}
66+
67+
/**
68+
* @param {import("acorn").UnaryExpression} node
69+
*/
70+
const visitUnaryExpression = (node) => {
71+
const argument = visitExpression(node.argument);
72+
if (argument === true) {
73+
return false;
74+
} else if (argument === false) {
75+
return true;
76+
}
77+
}
78+
79+
/**
80+
* @param {import("acorn").CallExpression} node
81+
*/
82+
const visitCallExpression = (node) => {
83+
const isDefinedExperimentalCall =
84+
// is `defined(arg)` call
85+
node.callee.type === 'Identifier' && node.callee.name === 'defined' && node.arguments.length == 1
86+
// and that arg is `NAPI_EXPERIMENTAL`
87+
&& node.arguments[0].type === 'Identifier' && node.arguments[0].name === 'NAPI_EXPERIMENTAL';
88+
89+
if (isDefinedExperimentalCall) {
90+
return false;
91+
}
92+
}
93+
94+
module.exports = {
95+
evaluate
96+
};

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@
3333
}
3434
],
3535
"description": "Node-API headers",
36-
"dependencies": {},
37-
"devDependencies": {},
36+
"devDependencies": {
37+
"acorn": "^8.12.1"
38+
},
3839
"directories": {},
3940
"gypfile": false,
4041
"homepage": "https://github.com/nodejs/node-api-headers",
4142
"keywords": [],
4243
"license": "MIT",
4344
"main": "index.js",
4445
"name": "node-api-headers",
45-
"optionalDependencies": {},
4646
"readme": "README.md",
4747
"repository": {
4848
"type": "git",
@@ -51,7 +51,8 @@
5151
"scripts": {
5252
"update-headers": "node --no-warnings scripts/update-headers.js",
5353
"write-symbols": "node --no-warnings scripts/write-symbols.js",
54-
"write-win32-def": "node --no-warnings scripts/write-win32-def.js"
54+
"write-win32-def": "node --no-warnings scripts/write-win32-def.js",
55+
"test": "node test/parse-utils.js "
5556
},
5657
"version": "1.3.0",
5758
"support": true

scripts/update-headers.js

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { parseArgs } = require('util')
77
const { createInterface } = require('readline');
88
const { inspect } = require('util');
99
const { runClang } = require('./clang-utils');
10+
const { evaluate } = require('../lib/parse-utils');
1011

1112
/**
1213
* @returns {Promise<string>} Version string, eg. `'v19.6.0'`.
@@ -32,8 +33,11 @@ function removeExperimentals(stream, destination, verbose = false) {
3233
};
3334
const rl = createInterface(stream);
3435

35-
/** @type {Array<'write' | 'ignore'>} */
36-
let mode = ['write'];
36+
/** @type {Array<'write' | 'ignore' | 'preprocessor'>} */
37+
const mode = ['write'];
38+
39+
/** @type {Array<string>} */
40+
const preprocessor = [];
3741

3842
/** @type {Array<string>} */
3943
const macroStack = [];
@@ -44,6 +48,22 @@ function removeExperimentals(stream, destination, verbose = false) {
4448
let lineNumber = 0;
4549
let toWrite = '';
4650

51+
const handlePreprocessor = (expression) => {
52+
const result = evaluate(expression);
53+
54+
macroStack.push(expression);
55+
56+
if (result === false) {
57+
debug(`Line ${lineNumber} Ignored '${expression}'`);
58+
mode.push('ignore');
59+
return false;
60+
} else {
61+
debug(`Line ${lineNumber} Pushed '${expression}'`);
62+
mode.push('write');
63+
return true;
64+
}
65+
};
66+
4767
rl.on('line', function lineHandler(line) {
4868
++lineNumber;
4969
if (matches = line.match(/^\s*#if(n)?def\s+([A-Za-z_][A-Za-z0-9_]*)/)) {
@@ -63,14 +83,23 @@ function removeExperimentals(stream, destination, verbose = false) {
6383
} else {
6484
mode.push('write');
6585
}
66-
6786
}
6887
else if (matches = line.match(/^\s*#if\s+(.+)$/)) {
69-
const identifier = matches[1];
70-
macroStack.push(identifier);
71-
mode.push('write');
88+
const expression = matches[1];
89+
if (expression.endsWith('\\')) {
90+
if (preprocessor.length) {
91+
reject(new Error(`Unexpected preprocessor continuation on line ${lineNumber}`));
92+
return;
93+
}
94+
preprocessor.push(expression.substring(0, expression.length - 1));
7295

73-
debug(`Line ${lineNumber} Pushed ${identifier}`);
96+
mode.push('preprocessor');
97+
return;
98+
} else {
99+
if (!handlePreprocessor(expression)) {
100+
return;
101+
}
102+
}
74103
}
75104
else if (line.match(/^#else(?:\s+|$)/)) {
76105
const identifier = macroStack[macroStack.length - 1];
@@ -83,7 +112,7 @@ function removeExperimentals(stream, destination, verbose = false) {
83112
return;
84113
}
85114

86-
if (identifier === 'NAPI_EXPERIMENTAL') {
115+
if (identifier.indexOf('NAPI_EXPERIMENTAL') > -1) {
87116
const lastMode = mode[mode.length - 1];
88117
mode[mode.length - 1] = (lastMode === 'ignore') ? 'write' : 'ignore';
89118
return;
@@ -98,9 +127,10 @@ function removeExperimentals(stream, destination, verbose = false) {
98127
if (!identifier) {
99128
rl.off('line', lineHandler);
100129
reject(new Error(`Macro stack is empty handling #endif on line ${lineNumber}`));
130+
return;
101131
}
102132

103-
if (identifier === 'NAPI_EXPERIMENTAL') {
133+
if (identifier.indexOf('NAPI_EXPERIMENTAL') > -1) {
104134
return;
105135
}
106136
}
@@ -113,7 +143,28 @@ function removeExperimentals(stream, destination, verbose = false) {
113143

114144
if (mode[mode.length - 1] === 'write') {
115145
toWrite += `${line}\n`;
146+
} else if (mode[mode.length - 1] === 'preprocessor') {
147+
if (!preprocessor) {
148+
reject(new Error(`Preprocessor mode without preprocessor on line ${lineNumber}`));
149+
return;
150+
}
151+
152+
if (line.endsWith('\\')) {
153+
preprocessor.push(line.substring(0, line.length - 1));
154+
return;
155+
}
156+
157+
preprocessor.push(line);
158+
159+
const expression = preprocessor.join('');
160+
preprocessor.length = 0;
161+
mode.pop();
162+
163+
if (!handlePreprocessor(expression)) {
164+
return;
165+
}
116166
}
167+
117168
});
118169

119170
rl.on('close', () => {
@@ -138,7 +189,7 @@ function removeExperimentals(stream, destination, verbose = false) {
138189
* @param {string} path Path for file to validate with clang.
139190
*/
140191
async function validateSyntax(path) {
141-
try {
192+
try {
142193
await runClang(['-fsyntax-only', path]);
143194
} catch (e) {
144195
throw new Error(`Syntax validation failed for ${path}: ${e}`);

test/parse-utils.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert');
3+
const { evaluate } = require("../lib/parse-utils");
4+
5+
/** @type {Array<[string, boolean | undefined]>} */
6+
const testCases = [
7+
[`defined(NAPI_EXPERIMENTAL)`, false],
8+
[`!defined(NAPI_EXPERIMENTAL)`, true],
9+
[`defined(NAPI_EXPERIMENTAL) || defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT)`, undefined],
10+
[`defined(NAPI_EXPERIMENTAL) && defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT)`, false],
11+
[`!defined(NAPI_EXPERIMENTAL) || (defined(NAPI_EXPERIMENTAL) && (defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT) || defined(NODE_API_EXPERIMENTAL_BASIC_ENV_OPT_OUT)))`, true],
12+
[`NAPI_VERSION >= 9`, undefined],
13+
[`!defined __cplusplus || (defined(_MSC_VER) && _MSC_VER < 1900)`, undefined], // parser error on `defined __cplusplus`
14+
];
15+
16+
for (const [text, expected] of testCases) {
17+
test(`${text} -> ${expected}`, (t) => {
18+
const result = evaluate(text);
19+
assert.strictEqual(result, expected);
20+
});
21+
}

0 commit comments

Comments
 (0)