diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f7e8f..d999003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to the "php-docblocker" extension will be documented in this file. ## [Unreleased] +- Supported variable docblock +```php +# Example + +/** @var int $var */ +$var = 1; + +/** @var stdClass $object */ +$object = new stdClass; + +/** @var \Closure $callback */ +$callback = function () {}; +$callback = fn () => 1; + +/** @var [type] $item */ +foreach ($data as $item) {} + +/** @var [type] $item */ +while ($item = array_pop($data)) {} +``` ## [2.7.0] - 2022-02-11 - Allow configuration of default type diff --git a/src/block/ForeachBlock.ts b/src/block/ForeachBlock.ts new file mode 100644 index 0000000..12cb2d3 --- /dev/null +++ b/src/block/ForeachBlock.ts @@ -0,0 +1,24 @@ +import { Doc, Param } from "../doc"; +import VariableBlock from "./VariableBlock"; + +/** + * Represents an var block for `foreach` + */ +export default class ForeachBlock extends VariableBlock +{ + + /** + * @inheritdoc + */ + protected pattern:RegExp = /^\s*foreach\s*\(.*?as\s+(\$[a-z_][a-z0-9_]*\s*=>\s*)?(\$[a-z_][a-z0-9_]*)\s*\)/im; + + /** + * @inheritdoc + */ + public parse():Doc + { + let params = this.match(); + return this.parseVar(params[2]); + } +} + diff --git a/src/block/VariableBlock.ts b/src/block/VariableBlock.ts new file mode 100644 index 0000000..fd9248a --- /dev/null +++ b/src/block/VariableBlock.ts @@ -0,0 +1,46 @@ +import { Block } from "../block"; +import { Doc, Param } from "../doc"; +import Config from "../util/config"; +import TypeUtil from "../util/TypeUtil"; + +/** + * Represents an var block + */ +export default class VariableBlock extends Block +{ + + /** + * @inheritdoc + */ + protected pattern:RegExp = /^\s*(\$[a-z0-9_]+)\s*\=?\s*([^;]*)/im; + + /** + * @inheritdoc + */ + public parse():Doc + { + let params = this.match(); + return this.parseVar(params[1], params[2]); + } + + /** + * parse + * + * @param key e.g.`$key` + * @param value + * @returns + */ + protected parseVar(key: string, value: any=undefined): Doc + { + let doc = new Doc(key.substring(1)); + if (value) { + doc.var = TypeUtil.instance.getTypeFromValue(value); + } else { + doc.var = TypeUtil.instance.getDefaultType(); + } + doc.inline = true; + + return doc; + } +} + diff --git a/src/block/WhileBlock.ts b/src/block/WhileBlock.ts new file mode 100644 index 0000000..7cc6430 --- /dev/null +++ b/src/block/WhileBlock.ts @@ -0,0 +1,24 @@ +import { Doc, Param } from "../doc"; +import VariableBlock from "./VariableBlock"; + +/** + * Represents an var block for `while` + */ +export default class WhileBlock extends VariableBlock +{ + + /** + * @inheritdoc + */ + protected pattern:RegExp = /^\s*while\s*\((\$[a-z0-9_]+)\s*=(?!=)/im; + + /** + * @inheritdoc + */ + public parse():Doc + { + let params = this.match(); + return this.parseVar(params[1]); + } +} + diff --git a/src/doc.ts b/src/doc.ts index 3338c5a..7473188 100644 --- a/src/doc.ts +++ b/src/doc.ts @@ -26,6 +26,13 @@ export class Doc */ public params:Array = []; + /** + * Doc inline + * + * Currently only used for variable block. + */ + public inline:boolean = false; + /** * Return tag * @@ -87,6 +94,9 @@ export class Doc if (input.message !== undefined) { this.message = input.message; } + if (input.inline !== undefined) { + this.inline = input.inline; + } if (input.params !== undefined && Array.isArray(input.params)) { input.params.forEach(param => { this.params.push(new Param(param.type, param.name)); @@ -222,22 +232,26 @@ export class Doc templateArray.pop(); } - let templateString:string = templateArray.join("\n"); + let templateString: string; + if (this.inline && templateArray.length === 3) { + templateString = "/** " + templateArray[2] + " \$" + templateArray[0] + " \${###} */"; + } else { + templateString = templateArray.join("\n"); + templateString = templateString.replace(/^$/gm, " *"); + templateString = templateString.replace(/^(?!(\s\*|\/\*))/gm, " * $1"); + if (Config.instance.get('autoClosingBrackets') == "never") { + templateString = "\n" + templateString + "\n */"; + } else { + templateString = "/**\n" + templateString + "\n */"; + } + } + let stop = 0; templateString = templateString.replace(/###/gm, function():string { stop++; return stop + ""; }); - templateString = templateString.replace(/^$/gm, " *"); - templateString = templateString.replace(/^(?!(\s\*|\/\*))/gm, " * $1"); - - if (Config.instance.get('autoClosingBrackets') == "never") { - templateString = "\n" + templateString + "\n */"; - } else { - templateString = "/**\n" + templateString + "\n */"; - } - let snippet = new SnippetString(templateString); return snippet; diff --git a/src/documenter.ts b/src/documenter.ts index 492cf3a..b570657 100644 --- a/src/documenter.ts +++ b/src/documenter.ts @@ -3,6 +3,9 @@ import FunctionBlock from "./block/function"; import Property from "./block/property"; import Class from "./block/class"; import {Doc, Param} from "./doc"; +import VariableBlock from "./block/VariableBlock"; +import ForeachBlock from "./block/ForeachBlock"; +import WhileBlock from "./block/WhileBlock"; /** * Check which type of docblock we need and instruct the components to build the @@ -59,6 +62,21 @@ export default class Documenter return cla.parse().build(); } + let variable = new VariableBlock(this.targetPosition, this.editor); + if (variable.test()) { + return variable.parse().build(); + } + + let foreach = new ForeachBlock(this.targetPosition, this.editor); + if (foreach.test()) { + return foreach.parse().build(); + } + + let while_ = new WhileBlock(this.targetPosition, this.editor); + if (while_.test()) { + return while_.parse().build(); + } + return new Doc().build(true); } } diff --git a/src/util/TypeUtil.ts b/src/util/TypeUtil.ts index dc0f68a..c3ca708 100644 --- a/src/util/TypeUtil.ts +++ b/src/util/TypeUtil.ts @@ -145,6 +145,11 @@ export default class TypeUtil { return 'integer'; } return 'int'; + case 'real': + case 'double': + return 'float'; + case 'unset': + return 'null'; default: return name; } @@ -159,15 +164,13 @@ export default class TypeUtil { */ public getTypeFromValue(value:string):string { - let result:Array; - - // Check for bool + // Check for bool `false` `true` `!exp` if (value.match(/^\s*(false|true)\s*$/i) !== null || value.match(/^\s*\!/i) !== null) { return this.getFormattedTypeByName('bool'); } - // Check for int - if (value.match(/^\s*([\d-]+)\s*$/) !== null) { + // Check for int `-1` `1` `1_000_000` + if (value.match(/^\s*(\-?\d[\d_]*)\s*$/) !== null) { return this.getFormattedTypeByName('int'); } @@ -176,6 +179,11 @@ export default class TypeUtil { return 'float'; } + // Check for float `.1` `1.1` `-1.1` `0.1_000_1` + if (value.match(/^\s*(\-?[\d_\.]*)\s*$/) !== null) { + return 'float'; + } + // Check for string if (value.match(/^\s*(["'])/) !== null || value.match(/^\s*<< { }); map.forEach(testData => { + if (testData.name === undefined) { + testData.name = testData.key; + } test("Completion: " + testData.name, () => { let pos:Position = testPositions[testData.key]; let result:any = completions.provideCompletionItems( @@ -31,10 +34,12 @@ suite("Completion tests", () => { let matched:Array = []; result.forEach(data => { - matched.push(data.label); + matched.push(data.insertText.value); }); + let actual = matched.join("\n"); + let expected = testData.result.join("\n"); - assert.deepEqual(testData.result, matched); + assert.deepEqual(actual, expected); }); }); }); diff --git a/test/fixtures/completions.php b/test/fixtures/completions.php index 5730f51..38f1a81 100644 --- a/test/fixtures/completions.php +++ b/test/fixtures/completions.php @@ -31,5 +31,25 @@ class Blah { } +////=> variable + /** + $var = null; + +////=> foreach + /** + foreach ([] as $key => $value) { + +////=> foreach-with-key + /** + foreach ([] as $value) { + +////=> while + /** + while ($value = array_shift($arrs)) { + +////=> while-no-var + /** + while ($value == true) { + ////=> empty /** diff --git a/test/fixtures/completions.php.json b/test/fixtures/completions.php.json index e60dcba..79f1f13 100644 --- a/test/fixtures/completions.php.json +++ b/test/fixtures/completions.php.json @@ -3,28 +3,28 @@ "key": "param", "name": "Param tag", "result": [ - "@param" + "@param ${1:mixed} $${2:name}" ] }, { "key": "return", "name": "Return tag", "result": [ - "@return" + "@return ${1:mixed}" ] }, { "key": "package", "name": "Package tag", "result": [ - "@package" + "@package ${1:category}" ] }, { "key": "var", "name": "Var tag", "result": [ - "@var" + "@var ${1:mixed}" ] }, { @@ -37,28 +37,72 @@ "key": "property", "name": "Property trigger", "result": [ - "/**" + "/**", + " * ${1:Undocumented variable}", + " *", + " * @var ${2:[type]}", + " */" ] }, { "key": "function", "name": "Function trigger", "result": [ - "/**" + "/**", + " * ${1:Undocumented function}", + " *", + " * @return ${2:void}", + " */" ] }, { "key": "class", "name": "Class trigger", "result": [ - "/**" + "/**", + " * ${1:Undocumented class}", + " */" + ] + }, + { + "key": "variable", + "result": [ + "/** @var ${1:[type]} $${2:var} ${3} */" + ] + }, + { + "key": "foreach", + "result": [ + "/** @var ${1:[type]} $${2:value} ${3} */" + ] + }, + { + "key": "foreach-with-key", + "result": [ + "/** @var ${1:[type]} $${2:value} ${3} */" + ] + }, + { + "key": "while", + "result": [ + "/** @var ${1:[type]} $${2:value} ${3} */" + ] + }, + { + "key": "while-no-var", + "result": [ + "/**", + " * ${1}", + " */" ] }, { "key": "empty", "name": "Empty trigger", "result": [ - "/**" + "/**", + " * ${1}", + " */" ] } ] diff --git a/test/fixtures/variables.php b/test/fixtures/variables.php new file mode 100644 index 0000000..3b9b117 --- /dev/null +++ b/test/fixtures/variables.php @@ -0,0 +1,91 @@ + doc +$var = 1; + +////=> float1 +$var = .3; + +////=> float2 +$var = 0.3; + +////=> float3 +$var = .1_000_1; + +////=> int1 +$var = 3; + +////=> int2 +$var = 03; + +////=> int3 +$var = 1_000_000; + +////=> bool1 +$var = true; + +////=> bool2 +$var = !0; + +////=> bool3 +$var = !'test'; + +////=> stdclass +$var = new stdClass; + +////=> object +$var = new class {}; + +////=> string1 +$var = 'string'; + +////=> string2 +$var = "string"; + +////=> string3 +$var = << closure +$var = function () {}; + +////=> closure-fn +$var = fn () => 123; + +////=> array1 +$var = [1,2,3]; + +////=> array2 +$var = []; + +////=> array3 +$var = array( + 1 +); + +////=> array4 +$var = [ + 1 +]; + +////=> type-casting-string +$var = (string)1; + +////=> type-casting-float1 +$var = (float)1; + +////=> type-casting-float2 +$var = (real)1; + +////=> type-casting-float3 +$var = (double)1; + +////=> type-casting-null +$var = (unset)1; + +////=> mixed1 +$var = test(); + +////=> mixed2 +$var = null; \ No newline at end of file diff --git a/test/fixtures/variables.php.json b/test/fixtures/variables.php.json new file mode 100644 index 0000000..2211d0e --- /dev/null +++ b/test/fixtures/variables.php.json @@ -0,0 +1,176 @@ +[ + { + "key": "doc", + "config": { + "inline": true + }, + "result": { + "inline": true, + "message": "var", + "var": "integer" + }, + "doc": "/** @var ${1:integer} $${2:var} ${3} */" + }, + { + "key": "float1", + "result": { + "var": "float" + } + }, + { + "key": "float2", + "result": { + "var": "float" + } + }, + { + "key": "float3", + "result": { + "var": "float" + } + }, + { + "key": "int1", + "result": { + "var": "integer" + } + }, + { + "key": "int2", + "result": { + "var": "integer" + } + }, + { + "key": "int3", + "result": { + "var": "integer" + } + }, + { + "key": "bool1", + "result": { + "var": "boolean" + } + }, + { + "key": "bool2", + "result": { + "var": "boolean" + } + }, + { + "key": "bool3", + "result": { + "var": "boolean" + } + }, + { + "key": "stdclass", + "result": { + "var": "stdClass" + } + }, + { + "key": "object", + "result": { + "var": "object" + } + }, + { + "key": "string1", + "result": { + "var": "string" + } + }, + { + "key": "string2", + "result": { + "var": "string" + } + }, + { + "key": "string3", + "result": { + "var": "string" + } + }, + { + "key": "closure", + "result": { + "var": "\\Closure" + } + }, + { + "key": "closure-fn", + "result": { + "var": "\\Closure" + } + }, + { + "key": "array2", + "result": { + "var": "array" + } + }, + { + "key": "array2", + "result": { + "var": "array" + } + }, + { + "key": "array3", + "result": { + "var": "array" + } + }, + { + "key": "array4", + "result": { + "var": "array" + } + }, + { + "key": "type-casting-string", + "result": { + "var": "string" + } + }, + { + "key": "type-casting-float1", + "result": { + "var": "float" + } + }, + { + "key": "type-casting-float2", + "result": { + "var": "float" + } + }, + { + "key": "type-casting-float3", + "result": { + "var": "float" + } + }, + { + "key": "type-casting-null", + "result": { + "var": "null" + } + }, + { + "key": "mixed1", + "result": { + "var": "[type]" + } + }, + { + "key": "mixed2", + "result": { + "var": "[type]" + } + } +] \ No newline at end of file diff --git a/test/variables.test.ts b/test/variables.test.ts new file mode 100644 index 0000000..fb29bee --- /dev/null +++ b/test/variables.test.ts @@ -0,0 +1,61 @@ +import * as assert from 'assert'; +import {TextEditor, TextDocument, WorkspaceConfiguration} from 'vscode'; +import Helper from './helpers'; +import Function from '../src/block/function'; +import {Doc, Param} from '../src/doc'; +import Config from '../src/util/config'; +import VariableBlock from '../src/block/VariableBlock'; + +suite("Variable tests", () => { + let editor:TextEditor; + let document:TextDocument; + let testPositions:any = {}; + + let defaults:Config = Helper.getConfig(); + let map = Helper.getFixtureMap('variables.php.json'); + + suiteSetup(function(done) { + Helper.loadFixture('variables.php', (edit:TextEditor, doc:TextDocument) => { + editor = edit; + document = doc; + testPositions = Helper.getFixturePositions(document); + done(); + }); + }); + + map.forEach(testData => { + if (testData.name === undefined) { + testData.name = testData.key; + } + + test("Match Test: "+ testData.name, () => { + let variable = new VariableBlock(testPositions[testData.key], editor); + assert.equal(variable.test(), true, test.name); + }); + + test("Parse Test: "+ testData.name, () => { + let variable = new VariableBlock(testPositions[testData.key], editor); + assert.ok(variable.parse(), test.name); + }); + + test("Type Test: "+ testData.name, () => { + Helper.setConfig(testData.config); + let variable = new VariableBlock(testPositions[testData.key], editor); + let actual:Doc = variable.parse(); + let expected:Doc = new Doc(testData.key.substring(1)); + if (testData.result.var === undefined) { + expected.var = undefined; + } + expected.fromObject(actual); + expected.fromObject(testData.result); + assert.deepEqual(actual, expected); + + if (testData.doc !== undefined) { + // if (isArray(testData.doc)) { + // testData.doc = testData.doc.join("\n"); + // } + assert.equal(actual.build().value, testData.doc, test.name); + } + }); + }); +});