Skip to content

Commit

Permalink
initial jsdoc support
Browse files Browse the repository at this point in the history
  • Loading branch information
stepankuzmin committed Aug 12, 2024
1 parent 5e17dd5 commit cffcbb7
Show file tree
Hide file tree
Showing 20 changed files with 660 additions and 23 deletions.
3 changes: 2 additions & 1 deletion bin/pbf
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import resolve from 'resolve-protobuf-schema';
import {compileRaw} from '../compile.js';

if (process.argv.length < 3) {
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--legacy]');
console.error('Usage: pbf [file.proto] [--no-read] [--no-write] [--jsdoc] [--legacy]');
process.exit(0);
}

const code = compileRaw(resolve.sync(process.argv[2]), {
noRead: process.argv.indexOf('--no-read') >= 0,
noWrite: process.argv.indexOf('--no-write') >= 0,
jsDoc: process.argv.indexOf('--jsdoc') >= 0,
legacy: process.argv.indexOf('--legacy') >= 0
});

Expand Down
110 changes: 91 additions & 19 deletions compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ export function compile(proto) {

export function compileRaw(proto, options = {}) {
const context = buildDefaults(buildContext(proto, null), proto.syntax);
return `${options.dev ? '' : `// code generated by pbf v${version}\n`}${writeContext(context, options)}`;

let output = options.dev ? '' : `// code generated by pbf v${version}\n`;
if (options.jsDoc) {
output += typeDef(`import("${options.dev ? '../../index.js' : 'pbf'}").default`, 'Pbf');
}
output += writeContext(context, options);
return output;
}

function writeContext(ctx, options) {
Expand All @@ -24,12 +30,20 @@ function writeContext(ctx, options) {
}

function writeMessage(ctx, options) {
const name = ctx._name;
const fields = ctx._proto.fields;

let code = '\n';

if (options.jsDoc) {
code += compileObjectType(ctx, name, fields);
}

if (!options.noRead) {
const readName = `read${ctx._name}`;
const readName = `read${name}`;
if (options.jsDoc) {
code += ['\n/**', ' * @param {Pbf} pbf', ' * @param {number} [end]', ` * @returns {${name}}`, ' */'].join('\n').concat('\n');
}
code +=
`${writeFunctionExport(options, readName)}function ${readName}(pbf, end) {
return pbf.readFields(${readName}Field, ${compileDest(ctx)}, end);
Expand Down Expand Up @@ -60,7 +74,12 @@ function ${readName}Field(tag, obj, pbf) {
}

if (!options.noWrite) {
const writeName = `write${ctx._name}`;
const writeName = `write${name}`;

if (options.jsDoc) {
code += ['\n/**', ` * @param {${name}} obj`, ' * @param {Pbf} pbf', ' */'].join('\n').concat('\n');
}

code += `${writeFunctionExport(options, writeName)}function ${writeName}(obj, pbf) {\n`;
for (const field of fields) {
const writeCode =
Expand Down Expand Up @@ -90,7 +109,10 @@ function getEnumValues(ctx) {
function writeEnum(ctx, {legacy}) {
const enums = JSON.stringify(getEnumValues(ctx), null, 4);
const name = ctx._name;
return `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`;

let output = '\n/** @enum {number} */';
output += `\n${legacy ? `const ${name} = exports.${name}` : `export const ${name}`} = ${enums};\n`;
return output;
}

function compileDest(ctx) {
Expand All @@ -114,23 +136,27 @@ function getType(ctx, field) {
return path.reduce((ctx, name) => ctx && ctx[name], ctx);
}

function fieldTypeIsNumber(field) {
switch (field.type) {
case 'float':
case 'double':
case 'uint32':
case 'uint64':
case 'int32':
case 'int64':
case 'sint32':
case 'sint64':
case 'fixed32':
case 'fixed64':
case 'sfixed32':
case 'sfixed64': return true;
default: return false;
}
}

function fieldShouldUseStringAsNumber(field) {
if (field.options.jstype === 'JS_STRING') {
switch (field.type) {
case 'float':
case 'double':
case 'uint32':
case 'uint64':
case 'int32':
case 'int64':
case 'sint32':
case 'sint64':
case 'fixed32':
case 'fixed64':
case 'sfixed32':
case 'sfixed64': return true;
default: return false;
}
return fieldTypeIsNumber(field);
}
return false;
}
Expand Down Expand Up @@ -415,3 +441,49 @@ function getDefaultWriteTest(ctx, field) {
function isPacked(field) {
return field.options.packed === 'true';
}

/**
* @param {{type: string}} field
* @returns {string}
*/
function getFieldType(field) {
let type = field.type;
if (fieldTypeIsNumber(field)) type = 'number';
if (field.type === 'bytes') type = 'Uint8Array';
if (field.type === 'bool') type = 'boolean';

return field.repeated ? `Array<${type}>` : type;
}

/**
* @param {string} type
* @param {string} name
* @param {{name: string; type: string; required: boolean}} [fields]
* @returns {string}
*/
function typeDef(type, name, fields = []) {
const properties = fields.map((field) => {
const type = getFieldType(field);
const name = field.required ? field.name : `[${field.name}]`;
return ` * @property {${type}} ${name}`;
});

return ['/**', ` * @typedef {${type}} ${name}`, ...properties, ' */']
.join('\n')
.concat('\n');
}

/**
* @param {object} ctx
* @param {string} name
* @param {{name: string; type: string; required: boolean}} [fields]
* @returns {string}
*/
function compileObjectType(ctx, name, fields = []) {
const typedFields = fields.map((field) => {
const type = getType(ctx, field);
return {...field, type: type ? type._name : field.type};
});

return typeDef('object', name, typedFields);
}
19 changes: 19 additions & 0 deletions package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"scripts": {
"bench": "node bench/bench.js",
"pretest": "eslint *.js compile.js test/*.js test/fixtures/*.js bin/pbf",
"test": "tsc && node --test",
"test": "tsc && node --test && tsc --allowJs --checkJs --noEmit --isolatedModules test/fixtures/*.js",
"cov": "node --test --experimental-test-covetage",
"build": "rollup -c",
"prepublishOnly": "npm run test && npm run build"
Expand Down Expand Up @@ -60,5 +60,8 @@
"rollup": "^4.18.0",
"tile-stats-runner": "^1.0.0",
"typescript": "^5.5.3"
}
},
"workspaces": [
"test/typescript/"
]
}
2 changes: 1 addition & 1 deletion test/compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('compiles all proto files to proper js', () => {
for (const path of files) {
if (!path.endsWith('.proto')) continue;
const proto = resolve(new URL(`fixtures/${path}`, import.meta.url));
const js = compileRaw(proto, {dev: true});
const js = compileRaw(proto, {dev: true, jsDoc: true});

// uncomment to update the fixtures
// fs.writeFileSync(new URL(`fixtures/${path}`.replace('.proto', '.js'), import.meta.url), js);
Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/defaults.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
/**
* @typedef {import("../../index.js").default} Pbf
*/

/** @enum {number} */
export const MessageType = {
"UNKNOWN": 0,
"GREETING": 1
};

/**
* @typedef {object} Envelope
* @property {MessageType} [type]
* @property {string} [name]
* @property {boolean} [flag]
* @property {number} [weight]
* @property {number} [id]
*/

/**
* @param {Pbf} pbf
* @param {number} [end]
* @returns {Envelope}
*/
export function readEnvelope(pbf, end) {
return pbf.readFields(readEnvelopeField, {type: 1, name: "test", flag: true, weight: 1.5, id: 1}, end);
}
Expand All @@ -14,6 +32,11 @@ function readEnvelopeField(tag, obj, pbf) {
else if (tag === 4) obj.weight = pbf.readFloat();
else if (tag === 5) obj.id = pbf.readVarint(true);
}

/**
* @param {Envelope} obj
* @param {Pbf} pbf
*/
export function writeEnvelope(obj, pbf) {
if (obj.type != null && obj.type !== 1) pbf.writeVarintField(1, obj.type);
if (obj.name != null && obj.name !== "test") pbf.writeStringField(2, obj.name);
Expand Down
42 changes: 42 additions & 0 deletions test/fixtures/defaults_implicit.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
/**
* @typedef {import("../../index.js").default} Pbf
*/

/** @enum {number} */
export const MessageType = {
"UNKNOWN": 0,
"GREETING": 1
};

/**
* @typedef {object} CustomType
*/

/**
* @param {Pbf} pbf
* @param {number} [end]
* @returns {CustomType}
*/
export function readCustomType(pbf, end) {
return pbf.readFields(readCustomTypeField, {}, end);
}
function readCustomTypeField(tag, obj, pbf) {
}

/**
* @param {CustomType} obj
* @param {Pbf} pbf
*/
export function writeCustomType(obj, pbf) {
}

/**
* @typedef {object} Envelope
* @property {MessageType} [type]
* @property {string} [name]
* @property {boolean} [flag]
* @property {number} [weight]
* @property {number} [id]
* @property {Array<string>} [tags]
* @property {Array<number>} [numbers]
* @property {Uint8Array} [bytes]
* @property {CustomType} [custom]
* @property {Array<MessageType>} [types]
*/

/**
* @param {Pbf} pbf
* @param {number} [end]
* @returns {Envelope}
*/
export function readEnvelope(pbf, end) {
return pbf.readFields(readEnvelopeField, {type: 0, name: "", flag: false, weight: 0, id: 0, tags: [], numbers: [], bytes: undefined, custom: undefined, types: []}, end);
}
Expand All @@ -27,6 +64,11 @@ function readEnvelopeField(tag, obj, pbf) {
else if (tag === 9) obj.custom = readCustomType(pbf, pbf.readVarint() + pbf.pos);
else if (tag === 10) pbf.readPackedVarint(obj.types);
}

/**
* @param {Envelope} obj
* @param {Pbf} pbf
*/
export function writeEnvelope(obj, pbf) {
if (obj.type) pbf.writeVarintField(1, obj.type);
if (obj.name) pbf.writeStringField(2, obj.name);
Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/defaults_proto3.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
/**
* @typedef {import("../../index.js").default} Pbf
*/

/** @enum {number} */
export const MessageType = {
"UNKNOWN": 0,
"GREETING": 1
};

/**
* @typedef {object} Envelope
* @property {MessageType} [type]
* @property {string} [name]
* @property {boolean} [flag]
* @property {number} [weight]
* @property {number} [id]
*/

/**
* @param {Pbf} pbf
* @param {number} [end]
* @returns {Envelope}
*/
export function readEnvelope(pbf, end) {
return pbf.readFields(readEnvelopeField, {type: 0, name: "", flag: false, weight: 0, id: 0}, end);
}
Expand All @@ -14,6 +32,11 @@ function readEnvelopeField(tag, obj, pbf) {
else if (tag === 4) obj.weight = pbf.readFloat();
else if (tag === 5) obj.id = pbf.readVarint(true);
}

/**
* @param {Envelope} obj
* @param {Pbf} pbf
*/
export function writeEnvelope(obj, pbf) {
if (obj.type) pbf.writeVarintField(1, obj.type);
if (obj.name) pbf.writeStringField(2, obj.name);
Expand Down
Loading

0 comments on commit cffcbb7

Please sign in to comment.