Skip to content

Commit e1b8feb

Browse files
authored
feat(no-release): add conditional support in #if (#52)
* feat: support conditionals in `#if` * refactor: move clang-utils to lib folder * ci: add test github job
1 parent 1294543 commit e1b8feb

10 files changed

+228
-16
lines changed

.github/workflows/sync-headers.yml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
- uses: actions/setup-node@v3
1919
with:
2020
node-version: 18
21+
- run: npm install
2122
- shell: bash
2223
id: check-changes
2324
name: Check Changes

.github/workflows/test.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Test
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
name: Test
9+
steps:
10+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
11+
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
12+
with:
13+
node-version: 18
14+
- run: npm install && npm test

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode
2+
node_modules/
3+
File renamed without changes.

lib/parse-utils.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
return visitExpression(expressionStatement.expression);
19+
} catch {
20+
// Return an unknown result if parsing failed
21+
return undefined;
22+
}
23+
}
24+
25+
/**
26+
* @param {import("acorn").Expression} node
27+
*/
28+
const visitExpression = (node) => {
29+
if (node.type === "LogicalExpression") {
30+
return visitLogicalExpression(node);
31+
} else if (node.type === "UnaryExpression") {
32+
return visitUnaryExpression(node);
33+
} else if (node.type === "CallExpression") {
34+
return visitCallExpression(node);
35+
} else {
36+
throw new Error(`Unknown node type: ${node.type} ${JSON.stringify(node)}`);
37+
}
38+
};
39+
40+
/**
41+
* @param {import("acorn").LogicalExpression} node
42+
*/
43+
const visitLogicalExpression = (node) => {
44+
const left = visitExpression(node.left);
45+
const right = visitExpression(node.right);
46+
47+
if (node.operator === "&&") {
48+
// We can shortcircuit regardless of `unknown` if either are false.
49+
if (left === false || right === false) {
50+
return false;
51+
} else if (left === undefined || right === undefined) {
52+
return undefined;
53+
} else {
54+
return left && right;
55+
}
56+
} else if (node.operator === "||") {
57+
if (left === undefined || right === undefined) {
58+
return undefined;
59+
} else {
60+
return left || right;
61+
}
62+
}
63+
};
64+
65+
/**
66+
* @param {import("acorn").UnaryExpression} node
67+
*/
68+
const visitUnaryExpression = (node) => {
69+
const argument = visitExpression(node.argument);
70+
if (typeof argument === 'boolean') {
71+
return !argument;
72+
}
73+
};
74+
75+
/**
76+
* @param {import("acorn").CallExpression} node
77+
*/
78+
const visitCallExpression = (node) => {
79+
const isDefinedExperimentalCall =
80+
// is `defined(arg)` call
81+
node.callee.type === 'Identifier' && node.callee.name === 'defined' && node.arguments.length == 1
82+
// and that arg is `NAPI_EXPERIMENTAL`
83+
&& node.arguments[0].type === 'Identifier' && node.arguments[0].name === 'NAPI_EXPERIMENTAL';
84+
85+
if (isDefinedExperimentalCall) {
86+
return false;
87+
}
88+
};
89+
90+
module.exports = {
91+
evaluate
92+
};

package-lock.json

+29
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-4
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

+62-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const { resolve } = require('path');
66
const { parseArgs } = require('util')
77
const { createInterface } = require('readline');
88
const { inspect } = require('util');
9-
const { runClang } = require('./clang-utils');
9+
const { runClang } = require('../lib/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}`);

scripts/write-symbols.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const { resolve: resolvePath } = require('path');
44
const { writeFile } = require('fs/promises');
5-
const { runClang } = require('./clang-utils');
5+
const { runClang } = require('../lib/clang-utils');
66

77
/** @typedef {{ js_native_api_symbols: string[]; node_api_symbols: string[]; }} SymbolInfo */
88

test/parse-utils.js

+21
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)