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

feat(no-release): add conditional support in #if #52

Merged
merged 3 commits into from
Oct 29, 2024
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 .github/workflows/sync-headers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- shell: bash
id: check-changes
name: Check Changes
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Test

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
name: Test
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 18
- run: npm install && npm test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.vscode
node_modules/

File renamed without changes.
92 changes: 92 additions & 0 deletions lib/parse-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const parser = require("acorn");

/**
* @param {string} text Code to evaluate
* @returns {boolean | undefined} The result of the evaluation, `undefined` if
* parsing failed or the result is unknown.
*/
function evaluate(text) {
try {
const ast = parser.parse(text, { ecmaVersion: 2020 });

const expressionStatement = ast.body[0];

if (expressionStatement.type !== "ExpressionStatement") {
throw new Error("Expected an ExpressionStatement");
}

return visitExpression(expressionStatement.expression);
} catch {
// Return an unknown result if parsing failed
return undefined;
}
}

/**
* @param {import("acorn").Expression} node
*/
const visitExpression = (node) => {
if (node.type === "LogicalExpression") {
return visitLogicalExpression(node);
} else if (node.type === "UnaryExpression") {
return visitUnaryExpression(node);
} else if (node.type === "CallExpression") {
return visitCallExpression(node);
} else {
throw new Error(`Unknown node type: ${node.type} ${JSON.stringify(node)}`);
}
};

/**
* @param {import("acorn").LogicalExpression} node
*/
const visitLogicalExpression = (node) => {
const left = visitExpression(node.left);
const right = visitExpression(node.right);

if (node.operator === "&&") {
// We can shortcircuit regardless of `unknown` if either are false.
if (left === false || right === false) {
return false;
} else if (left === undefined || right === undefined) {
return undefined;
} else {
return left && right;
}
} else if (node.operator === "||") {
if (left === undefined || right === undefined) {
return undefined;
} else {
return left || right;
}
}
};

/**
* @param {import("acorn").UnaryExpression} node
*/
const visitUnaryExpression = (node) => {
const argument = visitExpression(node.argument);
if (typeof argument === 'boolean') {
return !argument;
}
};

/**
* @param {import("acorn").CallExpression} node
*/
const visitCallExpression = (node) => {
const isDefinedExperimentalCall =
// is `defined(arg)` call
node.callee.type === 'Identifier' && node.callee.name === 'defined' && node.arguments.length == 1
// and that arg is `NAPI_EXPERIMENTAL`
&& node.arguments[0].type === 'Identifier' && node.arguments[0].name === 'NAPI_EXPERIMENTAL';

if (isDefinedExperimentalCall) {
return false;
}
};

module.exports = {
evaluate
};
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@
}
],
"description": "Node-API headers",
"dependencies": {},
"devDependencies": {},
"devDependencies": {
"acorn": "^8.12.1"
},
"directories": {},
"gypfile": false,
"homepage": "https://github.com/nodejs/node-api-headers",
"keywords": [],
"license": "MIT",
"main": "index.js",
"name": "node-api-headers",
"optionalDependencies": {},
"readme": "README.md",
"repository": {
"type": "git",
Expand All @@ -51,7 +51,8 @@
"scripts": {
"update-headers": "node --no-warnings scripts/update-headers.js",
"write-symbols": "node --no-warnings scripts/write-symbols.js",
"write-win32-def": "node --no-warnings scripts/write-win32-def.js"
"write-win32-def": "node --no-warnings scripts/write-win32-def.js",
"test": "node test/parse-utils.js "
},
"version": "1.3.0",
"support": true
Expand Down
73 changes: 62 additions & 11 deletions scripts/update-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const { resolve } = require('path');
const { parseArgs } = require('util')
const { createInterface } = require('readline');
const { inspect } = require('util');
const { runClang } = require('./clang-utils');
const { runClang } = require('../lib/clang-utils');
const { evaluate } = require('../lib/parse-utils');

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

/** @type {Array<'write' | 'ignore'>} */
let mode = ['write'];
/** @type {Array<'write' | 'ignore' | 'preprocessor'>} */
const mode = ['write'];

/** @type {Array<string>} */
const preprocessor = [];

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

const handlePreprocessor = (expression) => {
const result = evaluate(expression);

macroStack.push(expression);

if (result === false) {
debug(`Line ${lineNumber} Ignored '${expression}'`);
mode.push('ignore');
return false;
} else {
debug(`Line ${lineNumber} Pushed '${expression}'`);
mode.push('write');
return true;
}
};

rl.on('line', function lineHandler(line) {
++lineNumber;
if (matches = line.match(/^\s*#if(n)?def\s+([A-Za-z_][A-Za-z0-9_]*)/)) {
Expand All @@ -63,14 +83,23 @@ function removeExperimentals(stream, destination, verbose = false) {
} else {
mode.push('write');
}

}
else if (matches = line.match(/^\s*#if\s+(.+)$/)) {
const identifier = matches[1];
macroStack.push(identifier);
mode.push('write');
const expression = matches[1];
if (expression.endsWith('\\')) {
if (preprocessor.length) {
reject(new Error(`Unexpected preprocessor continuation on line ${lineNumber}`));
return;
}
preprocessor.push(expression.substring(0, expression.length - 1));

debug(`Line ${lineNumber} Pushed ${identifier}`);
mode.push('preprocessor');
return;
} else {
if (!handlePreprocessor(expression)) {
return;
}
}
}
else if (line.match(/^#else(?:\s+|$)/)) {
const identifier = macroStack[macroStack.length - 1];
Expand All @@ -83,7 +112,7 @@ function removeExperimentals(stream, destination, verbose = false) {
return;
}

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

if (identifier === 'NAPI_EXPERIMENTAL') {
if (identifier.indexOf('NAPI_EXPERIMENTAL') > -1) {
return;
}
}
Expand All @@ -113,7 +143,28 @@ function removeExperimentals(stream, destination, verbose = false) {

if (mode[mode.length - 1] === 'write') {
toWrite += `${line}\n`;
} else if (mode[mode.length - 1] === 'preprocessor') {
if (!preprocessor) {
reject(new Error(`Preprocessor mode without preprocessor on line ${lineNumber}`));
return;
}

if (line.endsWith('\\')) {
preprocessor.push(line.substring(0, line.length - 1));
return;
}

preprocessor.push(line);

const expression = preprocessor.join('');
preprocessor.length = 0;
mode.pop();

if (!handlePreprocessor(expression)) {
return;
}
}

});

rl.on('close', () => {
Expand All @@ -138,7 +189,7 @@ function removeExperimentals(stream, destination, verbose = false) {
* @param {string} path Path for file to validate with clang.
*/
async function validateSyntax(path) {
try {
try {
await runClang(['-fsyntax-only', path]);
} catch (e) {
throw new Error(`Syntax validation failed for ${path}: ${e}`);
Expand Down
2 changes: 1 addition & 1 deletion scripts/write-symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

Expand Down
21 changes: 21 additions & 0 deletions test/parse-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const test = require('node:test');
const assert = require('node:assert');
const { evaluate } = require("../lib/parse-utils");

/** @type {Array<[string, boolean | undefined]>} */
const testCases = [
[`defined(NAPI_EXPERIMENTAL)`, false],
[`!defined(NAPI_EXPERIMENTAL)`, true],
[`defined(NAPI_EXPERIMENTAL) || defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT)`, undefined],
[`defined(NAPI_EXPERIMENTAL) && defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT)`, false],
[`!defined(NAPI_EXPERIMENTAL) || (defined(NAPI_EXPERIMENTAL) && (defined(NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT) || defined(NODE_API_EXPERIMENTAL_BASIC_ENV_OPT_OUT)))`, true],
[`NAPI_VERSION >= 9`, undefined],
[`!defined __cplusplus || (defined(_MSC_VER) && _MSC_VER < 1900)`, undefined], // parser error on `defined __cplusplus`
];

for (const [text, expected] of testCases) {
test(`${text} -> ${expected}`, (t) => {
const result = evaluate(text);
assert.strictEqual(result, expected);
});
}