Skip to content
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
241 changes: 118 additions & 123 deletions packages/cli/lib/ast-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,145 +4,140 @@
// License text available at https://opensource.org/licenses/MIT

'use strict';
const tsquery = require('@phenomnomnominal/tsquery').tsquery;
const debug = require('./debug')('ast-query');

const tsArtifact = {
ClassDeclaration: 'ClassDeclaration',
PropertyDeclaration: 'PropertyDeclaration',
Identifier: 'Identifier',
Decorator: 'Decorator',
CallExpression: 'CallExpression',
ObjectLiteralExpression: 'ObjectLiteralExpression',
PropertyAssignment: 'PropertyAssignment',
TrueKeywordTrue: 'TrueKeyword[value="true"]',
// the following are placed in case it is needed to explore more artifacts
IfStatement: 'IfStatement',
ForStatement: 'ForStatement',
WhileStatement: 'WhileStatement',
DoStatement: 'DoStatement',
VariableStatement: 'VariableStatement',
FunctionDeclaration: 'FunctionDeclaration',
ArrowFunction: 'ArrowFunction',
ImportDeclaration: 'ImportDeclaration',
StringLiteral: 'StringLiteral',
FalseKeyword: 'FalseKeyword',
NullKeyword: 'NullKeyword',
AnyKeyword: 'AnyKeyword',
NumericLiteral: 'NumericLiteral',
NoSubstitutionTemplateLiteral: 'NoSubstitutionTemplateLiteral',
TemplateExpression: 'TemplateExpression',
};

const RootNodesFindID = [
// Defaul format generated by lb4 model
[
tsArtifact.ClassDeclaration,
tsArtifact.PropertyDeclaration,
tsArtifact.Identifier,
],
// Model JSON definition inside the @model decorator
[
tsArtifact.ClassDeclaration,
tsArtifact.Decorator,
tsArtifact.CallExpression,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.Identifier,
],
// Model JSON static definition inside the Class
[
tsArtifact.ClassDeclaration,
tsArtifact.PropertyDeclaration,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.Identifier,
],
];
const ChildNodesFindID = [
// Defaul format generated by lb4 model
[
tsArtifact.ClassDeclaration,
tsArtifact.PropertyDeclaration,
tsArtifact.Decorator,
tsArtifact.CallExpression,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.TrueKeywordTrue,
],

// Model JSON definition inside the @model decorator
[
tsArtifact.ClassDeclaration,
tsArtifact.Decorator,
tsArtifact.CallExpression,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.TrueKeywordTrue,
],
// Model JSON static definition inside the Class
[
tsArtifact.ClassDeclaration,
tsArtifact.PropertyDeclaration,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.ObjectLiteralExpression,
tsArtifact.PropertyAssignment,
tsArtifact.TrueKeywordTrue,
],
];
const {tsquery} = require('@phenomnomnominal/tsquery');
const {syntaxKindName} = tsquery;
const debug = require('./debug')('ast-query');

/**
* Parse the file using the possible formats specified in the arrays
* rootNodesFindID and childNodesFindID
* @param {string} fileContent with a model.ts class
*/
exports.getIdFromModel = async function(fileContent) {
let nodePos = 0;
let retVal = null;

exports.getIdFromModel = function(fileContent) {
const ast = tsquery.ast(fileContent);
for (const rootNodes of RootNodesFindID) {
const propertyArr = [];
const stRootNode = rootNodes.join('>');
const nodes = tsquery(ast, stRootNode);
for (const queryName in QUERIES) {
debug('Trying %s', queryName);
const {query, getModelPropertyDeclaration} = QUERIES[queryName];

debug(`rootNode ${stRootNode}`);
const idFieldAssignments = tsquery(ast, query);

for (const a of nodes) {
propertyArr.push(a.escapedText);
}
for (const node of idFieldAssignments) {
const fieldName = node.name.escapedText;
/* istanbul ignore if */
if (debug.enabled) {
debug(
' trying prop metadata field "%s" with value `%s`',
fieldName,
getNodeSource(node),
);
}

const stChildNode = ChildNodesFindID[nodePos].join('>');
const subnodes = tsquery(ast, stChildNode);
if (!isPrimaryKeyFlag(node.initializer)) continue;

let i = 0;
for (const a of subnodes) {
if (a.parent.name.escapedText === 'id') {
// we found the primary key for the model
retVal = propertyArr[i];
debug(`found key: ${retVal}`);
break;
const propDeclarationNode = getModelPropertyDeclaration(node);
const modelPropertyName = propDeclarationNode.name.escapedText;
/* istanbul ignore if */
if (debug.enabled) {
debug(
'Found primary key `%s` with id flag set to `%s`',
modelPropertyName,
getNodeSource(node),
);
}
i++;
}

if (retVal !== null) {
break;
return modelPropertyName;
}
}

nodePos++;
// no primary key was found
return null;

function getNodeSource(node) {
return fileContent.slice(node.pos, node.end).trim();
}
};

return retVal;
const QUERIES = {
'default format generated by lb4 model': {
// @property({id: true|1})
// id: number
query:
// Find all class properties decorated with `@property()`
'ClassDeclaration>PropertyDeclaration>Decorator:has([name="id"])>' +
// Find object-literal argument passed to `@property` decorator
'CallExpression>ObjectLiteralExpression>' +
// Find all assignments to `id` property (metadata field)
'PropertyAssignment:has([name="id"])',

getModelPropertyDeclaration(node) {
return node.parent.parent.parent.parent;
},
},

'model JSON definition inside the @model decorator': {
// @model({properties: {id: {type:number, id:true|1}}})
query:
// Find all classes decorated with `@model()`
'ClassDeclaration>Decorator:has([name="model"])>' +
// Find object-literal argument passed to `@model` decorator
'CallExpression>ObjectLiteralExpression>' +
// Find {properties:{...}} initializer
'PropertyAssignment:has([name="properties"])>ObjectLiteralExpression>' +
// Find all model properties, e.g. {name: {required: true}}
'PropertyAssignment>ObjectLiteralExpression>' +
// Find all assignments to `id` property (metadata field)
'PropertyAssignment:has([name="id"])',
getModelPropertyDeclaration(node) {
return node.parent.parent;
},
},

'model JSON definition inside a static model property "definition"': {
// static definition = {properties: {id: {type:number, id:true|1}}}
query:
// Find all classes with static property `definition`
// TODO: check for "static" modifier
'ClassDeclaration>PropertyDeclaration:has([name="definition"])>' +
// Find object-literal argument used to initialize `definition`
'ObjectLiteralExpression>' +
// Find {properties:{...}} initializer
'PropertyAssignment:has([name="properties"])>ObjectLiteralExpression>' +
// Find all model properties, e.g. {name: {required: true}}
'PropertyAssignment>ObjectLiteralExpression>' +
// Find all assignments to `id` property (metadata field)
'PropertyAssignment:has([name="id"])',

getModelPropertyDeclaration(node) {
return node.parent.parent;
},
},
};

function isPrimaryKeyFlag(idInitializer) {
const kindName = syntaxKindName(idInitializer.kind);

/* istanbul ignore if */
if (debug.enabled) {
debug(
'Checking primary key flag initializer, kind: %s node:',
kindName,
require('util').inspect(
{...idInitializer, parent: '[removed for brevity]'},
{depth: null},
),
);
}

// {id: true}
if (kindName === 'TrueKeyword') return true;

// {id: number}
if (kindName === 'NumericLiteral') {
const ix = +idInitializer.text;
// the value must be a non-zero number, e.g. {id: 1}
return ix !== 0 && !isNaN(ix);
}

return false;
}
Loading