diff --git a/.changeset/silver-waves-exercise.md b/.changeset/silver-waves-exercise.md new file mode 100644 index 00000000..d1feec48 --- /dev/null +++ b/.changeset/silver-waves-exercise.md @@ -0,0 +1,5 @@ +--- +"@plutolang/pyright-deducer": patch +--- + +feat(deducer): support basic unary operation diff --git a/components/deducers/python-pyright/src/value-evaluator/value-tree-builder.ts b/components/deducers/python-pyright/src/value-evaluator/value-tree-builder.ts index f72e4f01..63823f7e 100644 --- a/components/deducers/python-pyright/src/value-evaluator/value-tree-builder.ts +++ b/components/deducers/python-pyright/src/value-evaluator/value-tree-builder.ts @@ -15,6 +15,7 @@ import { StringNode, TupleNode, TypeAnnotationNode, + UnaryOperationNode, } from "pyright-internal/dist/parser/parseNodes"; import { KeywordType } from "pyright-internal/dist/parser/tokenizerTypes"; import { TypeEvaluator } from "pyright-internal/dist/analyzer/typeEvaluatorTypes"; @@ -32,6 +33,7 @@ import { StringTreeNode, TreeNode, TupleTreeNode, + UnaryOperationTreeNode, } from "./value-tree-types"; import { getNodeText } from "./utils"; @@ -50,7 +52,7 @@ export class TreeBuilder { private readonly createNodeFunctions: { [NodeType in ParseNodeType]?: CreateNodeFunction } = { [ParseNodeType.Error]: this.unimplementedNode, - [ParseNodeType.UnaryOperation]: this.unimplementedNode, + [ParseNodeType.UnaryOperation]: this.createNodeForUnaryOperation, [ParseNodeType.BinaryOperation]: this.createNodeForBinaryOperation, [ParseNodeType.Assignment]: this.unimplementedNode, [ParseNodeType.TypeAnnotation]: this.createNodeForTypeAnnotation, @@ -217,6 +219,11 @@ export class TreeBuilder { return DictionaryTreeNode.create(node, items); } + private createNodeForUnaryOperation(node: UnaryOperationNode): UnaryOperationTreeNode { + const expression = this.createNode(node.expression); + return UnaryOperationTreeNode.create(node, expression); + } + private createNodeForBinaryOperation(node: BinaryOperationNode): BinaryOperationTreeNode { const leftNode = this.createNode(node.leftExpression); const rightNode = this.createNode(node.rightExpression); @@ -245,7 +252,9 @@ export class TreeBuilder { private unimplementedNode(node: ParseNode): never { throw new Error( - `The creation of node type '${node.nodeType}' is not implemented yet. If you need this feature, please submit an issue.` + `${getNodeText(node)}: The creation of node type '${ + node.nodeType + }' is not implemented yet. If you need this feature, please submit an issue.` ); } } diff --git a/components/deducers/python-pyright/src/value-evaluator/value-tree-evaluator.ts b/components/deducers/python-pyright/src/value-evaluator/value-tree-evaluator.ts index 234db482..f28ffdbb 100644 --- a/components/deducers/python-pyright/src/value-evaluator/value-tree-evaluator.ts +++ b/components/deducers/python-pyright/src/value-evaluator/value-tree-evaluator.ts @@ -18,6 +18,7 @@ import { TreeNodeBase, TreeNodeFlags, TupleTreeNode, + UnaryOperationTreeNode, } from "./value-tree-types"; import { getNodeText } from "./utils"; import { @@ -43,7 +44,7 @@ export class TreeEvaluator { private readonly createNodeFunctions: { [NodeType in ParseNodeType]?: EvaluateFunction } = { [ParseNodeType.Error]: this.unimplementedNode, - [ParseNodeType.UnaryOperation]: this.unimplementedNode, + [ParseNodeType.UnaryOperation]: this.evaluateForUnaryOperation, [ParseNodeType.BinaryOperation]: this.evaluateForBinaryOperation, [ParseNodeType.Assignment]: this.unimplementedNode, [ParseNodeType.TypeAnnotation]: this.unimplementedNode, @@ -113,7 +114,7 @@ export class TreeEvaluator { // Currently, we only support the environment variable access using the index node. if (node.flags && node.flags & TreeNodeFlags.AccessEnvVar) { const value = this.evaluate(node.items[0], fillings); - if (!Value.isStringLiteral(value)) { + if (!Value.isLiteral(value) || !Value.isStringLiteral(value)) { throw new Error(`${getNodeText(node.node)}: Only support string literal access as index.`); } @@ -140,7 +141,7 @@ export class TreeEvaluator { const result = node.strings .map((str) => { const part = this.evaluate(str, fillings); - if (!Value.isStringLiteral(part)) { + if (!Value.isLiteral(part) || !Value.isStringLiteral(part)) { throw new Error(`${getNodeText(str.node)}: Only support string literal in string list.`); } return part.value; @@ -169,16 +170,22 @@ export class TreeEvaluator { (middleIdx >= node.node.middleTokens.length || node.node.fieldExpressions[fieldIdx].start < node.node.middleTokens[middleIdx].start); - if (isField && !Value.isStringLiteral(fieldValues[fieldIdx])) { - // prettier-ignore - throw new Error( - `${getNodeText(node.node)}: Only support string literal, not including the string returned by a function, in the format string.` - ); + // Check if the field is a string literal; if not, throw an error. + if (isField) { + const fieldValue = fieldValues[fieldIdx]; + if (!Value.isLiteral(fieldValue) || !Value.isStringLiteral(fieldValue)) { + // prettier-ignore + throw new Error( + `${getNodeText(node.node)}: Only support string literal, not including the string returned by a function, in the format string.` + ); + } + // Append the string literal value to the result. + result += fieldValue.value; + fieldIdx++; // Move to the next field index. + } else { + // If not a field, append the middle token's escaped value to the result. + result += node.node.middleTokens[middleIdx++].escapedValue; } - - result += isField - ? (fieldValues[fieldIdx++] as LiteralValue).value - : node.node.middleTokens[middleIdx++].escapedValue; } return LiteralValue.create(result); @@ -188,12 +195,15 @@ export class TreeEvaluator { if (node.flags && node.flags & TreeNodeFlags.AccessEnvVar) { // This expression is an environment variable access. const envVarName = this.evaluate(node.args[0], fillings); - if (!Value.isStringLiteral(envVarName)) { + if (!Value.isLiteral(envVarName) || !Value.isStringLiteral(envVarName)) { throw new Error(`${getNodeText(node.node)}: Only support string literal access as index.`); } const defaultValue = node.args[1] ? this.evaluate(node.args[1], fillings) : undefined; - if (defaultValue && !Value.isStringLiteral(defaultValue)) { + if ( + defaultValue && + (!Value.isLiteral(defaultValue) || !Value.isStringLiteral(defaultValue)) + ) { throw new Error( `${getNodeText( node.node @@ -238,6 +248,52 @@ export class TreeEvaluator { return LiteralValue.create(node.node.value); } + private evaluateForUnaryOperation(node: UnaryOperationTreeNode, fillings: Fillings) { + const value = this.evaluate(node.expression, fillings); + + if (!Value.isLiteral(value)) { + throw new Error(`${getNodeText(node.node)}: Only support literal values in unary operation.`); + } + + if (Value.isNumberLiteral(value)) { + const numberValue = value.value as number; + switch (node.node.operator) { + case OperatorType.Add: + return LiteralValue.create(numberValue); + case OperatorType.Subtract: + return LiteralValue.create(-numberValue); + default: + throw new Error( + `The operator '${node.node.operator}' is not supported yet. If you need this feature, please submit an issue.` + ); + } + } + + if (Value.isStringLiteral(value)) { + if (node.node.operator === OperatorType.Add) { + return value; + } + + throw new Error( + `${getNodeText(node.node)}: Only support the unary operation '+' for string literal.` + ); + } + + if (Value.isBooleanLiteral(value)) { + if (node.node.operator === OperatorType.Not) { + return LiteralValue.create(!value); + } + + throw new Error( + `${getNodeText(node.node)}: Only support the unary operation 'not' for boolean literal.` + ); + } + + throw new Error( + `${getNodeText(node.node)}: The unary operation is not supported for the value type.` + ); + } + private evaluateForBinaryOperation( node: BinaryOperationTreeNode, fillings: Fillings diff --git a/components/deducers/python-pyright/src/value-evaluator/value-tree-types.ts b/components/deducers/python-pyright/src/value-evaluator/value-tree-types.ts index 390c1a75..74c37127 100644 --- a/components/deducers/python-pyright/src/value-evaluator/value-tree-types.ts +++ b/components/deducers/python-pyright/src/value-evaluator/value-tree-types.ts @@ -14,6 +14,7 @@ import { StringListNode, StringNode, TupleNode, + UnaryOperationNode, } from "pyright-internal/dist/parser/parseNodes"; import * as AnalyzerNodeInfo from "pyright-internal/dist/analyzer/analyzerNodeInfo"; import { convertOffsetToPosition } from "pyright-internal/dist/common/positionUtils"; @@ -149,6 +150,26 @@ export namespace CallTreeNode { } } +export interface UnaryOperationTreeNode extends TreeNodeBase { + readonly nodeType: ParseNodeType.UnaryOperation; + readonly node: UnaryOperationNode; + readonly expression: TreeNode; +} + +export namespace UnaryOperationTreeNode { + export function create(parseNode: UnaryOperationNode, expression: TreeNode) { + const node: UnaryOperationTreeNode = { + nodeType: ParseNodeType.UnaryOperation, + node: parseNode, + expression: expression, + print: () => + `${getNodeText(parseNode, `UnaryOperation#${parseNode.operator}`)}(${expression.node.id})`, + }; + + return node; + } +} + export interface BinaryOperationTreeNode extends TreeNodeBase { readonly nodeType: ParseNodeType.BinaryOperation; readonly node: BinaryOperationNode; @@ -270,6 +291,7 @@ export type TreeNode = | TupleTreeNode | DictionaryTreeNode | CallTreeNode + | UnaryOperationTreeNode | BinaryOperationTreeNode | StringListTreeNode | StringTreeNode diff --git a/components/deducers/python-pyright/src/value-evaluator/value-types.ts b/components/deducers/python-pyright/src/value-evaluator/value-types.ts index f1756310..33e34d3a 100644 --- a/components/deducers/python-pyright/src/value-evaluator/value-types.ts +++ b/components/deducers/python-pyright/src/value-evaluator/value-types.ts @@ -112,12 +112,20 @@ export type Value = export namespace Value { // export function toJson(value: Value): string {} - export function isStringLiteral(value: Value): value is LiteralValue { - return value.valueType === ValueType.Literal && typeof value.value === "string"; + export function isLiteral(value: Value): value is LiteralValue { + return value.valueType === ValueType.Literal; } - export function isNumberLiteral(value: Value): value is LiteralValue { - return value.valueType === ValueType.Literal && typeof value.value === "number"; + export function isStringLiteral(value: LiteralValue) { + return typeof value.value === "string"; + } + + export function isNumberLiteral(value: LiteralValue) { + return typeof value.value === "number"; + } + + export function isBooleanLiteral(value: LiteralValue) { + return typeof value.value === "boolean"; } interface ToStringOptions {