├── .gitignore ├── README.md ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── CHANGELOG.md ├── src ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── diagnostic.ts ├── phpparser.ts └── extension.ts ├── tsconfig.json ├── .eslintrc.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode extension PHP extra checks 2 | 3 | Additional checks for PHP: 4 | - Every function has a return type 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "PHP extra checks" extension will be documented in this file. 4 | 5 | ## 1.0.0 6 | 7 | - Initial release, only one check is available: Missing function's return type declaration -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests', err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/diagnostic.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticCollection, ExtensionContext, Uri, Range, languages } from 'vscode'; 2 | 3 | let diagnosticCollection: DiagnosticCollection; 4 | let diagnosticMap: Map = new Map(); 5 | 6 | export function initDiagnostics(context:ExtensionContext) { 7 | diagnosticCollection = languages.createDiagnosticCollection('php-extra-checks'); 8 | } 9 | 10 | export function noReturnType(uri:Uri, range:Range) { 11 | addToDiagnostics(uri, 12 | new Diagnostic(range, 13 | `Missing function's return type declaration`, 14 | 2)); 15 | } 16 | 17 | export function clearDiagnostics(uri:Uri) { 18 | let canonicalFile = uri.path; 19 | diagnosticMap.set(canonicalFile, []); 20 | } 21 | 22 | export function addToDiagnostics(uri:Uri, diagnostic:Diagnostic) { 23 | let canonicalFile = uri.path; 24 | let diagnostics = diagnosticMap.get(canonicalFile); 25 | if (!diagnostics) { diagnostics = []; } 26 | diagnostics.push(diagnostic); 27 | diagnosticMap.set(canonicalFile, diagnostics); 28 | } 29 | 30 | export function finishDiagnostics(uri:Uri) { 31 | let canonicalFile = uri.path; 32 | let diagnostics = diagnosticMap.get(canonicalFile); 33 | diagnosticCollection.set(uri, diagnostics); 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-php-extra-checks", 3 | "displayName": "PHP extra checks", 4 | "description": "Can be installed together with another PHP extension if you need to add more checks", 5 | "version": "1.0.0", 6 | "publisher": "MarinaGlancy", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/marinaglancy/vscode-php-extra-checks" 10 | }, 11 | "engines": { 12 | "vscode": "^1.74.0" 13 | }, 14 | "categories": [ 15 | "Other" 16 | ], 17 | "activationEvents": [ 18 | "onLanguage:php" 19 | ], 20 | "main": "./out/extension.js", 21 | "scripts": { 22 | "vscode:prepublish": "npm run compile", 23 | "compile": "tsc -p ./", 24 | "watch": "tsc -watch -p ./", 25 | "pretest": "npm run compile && npm run lint", 26 | "lint": "eslint src --ext ts", 27 | "test": "node ./out/test/runTest.js" 28 | }, 29 | "devDependencies": { 30 | "@types/glob": "^8.1.0", 31 | "@types/mocha": "^10.0.1", 32 | "@types/node": "16.x", 33 | "@types/vscode": "^1.74.0", 34 | "@typescript-eslint/eslint-plugin": "^5.53.0", 35 | "@typescript-eslint/parser": "^5.53.0", 36 | "@vscode/test-electron": "^2.2.3", 37 | "eslint": "^8.34.0", 38 | "glob": "^8.1.0", 39 | "mocha": "^10.2.0", 40 | "typescript": "^4.9.5" 41 | }, 42 | "dependencies": { 43 | "php-parser": "^3.1.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/phpparser.ts: -------------------------------------------------------------------------------- 1 | const engine = require("php-parser"); 2 | 3 | export interface NodeLayout { 4 | children?: Array; 5 | [key: string]: any; 6 | }; 7 | 8 | const parser = new engine({ 9 | // some options : 10 | parser: { 11 | extractDoc: false, 12 | php7: true 13 | }, 14 | ast: { 15 | withPositions: true, 16 | withSource: true 17 | }, 18 | }); 19 | 20 | export function parseCode(phpFile:string|Buffer): NodeLayout { 21 | return parser.parseCode(phpFile); 22 | } 23 | 24 | export function walkAll(nl:NodeLayout, callback:(node:NodeLayout) => void) { 25 | callback(nl); 26 | let keys = Object.keys(nl); 27 | keys.forEach((key) => { 28 | if (key === 'kind' || key === 'loc') { return; } 29 | if (nl[key] === null || nl[key] === undefined) { 30 | } else if ((typeof nl[key]) === 'object') { 31 | walkAll(nl[key], callback); 32 | } 33 | }); 34 | } 35 | 36 | export function walk(nl:NodeLayout, validationCallback:(node:NodeLayout) => boolean): Array { 37 | if (validationCallback(nl)) { return [nl]; } 38 | let res:Array = []; 39 | let keys = Object.keys(nl); 40 | keys.forEach((key) => { 41 | if (key === 'kind' || key === 'loc') { return; } 42 | if (nl[key] === null || nl[key] === undefined) { 43 | } else if ((typeof nl[key]) === 'object') { 44 | res = [...res, ...walk(nl[key], validationCallback)]; 45 | } 46 | }); 47 | return res; 48 | } 49 | 50 | export function findAllFunctions(nl:NodeLayout): Array { 51 | let res:Array = []; 52 | walk(nl, (node) => { 53 | return node.kind === 'function' || node.kind === 'class'; 54 | }).forEach((node) => { 55 | if (node.kind === 'function') { 56 | res.push(node); 57 | } else { 58 | res = res.concat(node.body.filter((n:NodeLayout) => n.kind === 'method')); 59 | } 60 | }); 61 | return res; 62 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as PHPParser from './phpparser'; 3 | import path = require('path'); 4 | import * as MyDiagnostics from './diagnostic'; 5 | 6 | // This method is called when your extension is activated 7 | export async function activate(context: vscode.ExtensionContext) { 8 | 9 | MyDiagnostics.initDiagnostics(context); 10 | 11 | vscode.workspace.onDidOpenTextDocument((e:vscode.TextDocument) => { 12 | highlightProblems(e); 13 | }); 14 | 15 | vscode.window.onDidChangeActiveTextEditor((e) => { 16 | e && e.document && highlightProblems(e.document); 17 | }, null, context.subscriptions); 18 | 19 | vscode.workspace.onDidSaveTextDocument((e) => { 20 | e && e.uri && highlightProblemsAll(e.uri); 21 | }); 22 | 23 | vscode.workspace.onDidChangeTextDocument(e => { 24 | // TODO do not evaluate on each change for now. 25 | // e && e.contentChanges && e.contentChanges.length && e.document && 26 | // higlightProblemsAll(e.document.uri); 27 | }); 28 | 29 | highlightProblemsAll(); 30 | } 31 | 32 | // This method is called when your extension is deactivated 33 | export function deactivate() {} 34 | 35 | function highlightProblemsAll(uri:vscode.Uri|null = null) { 36 | vscode.workspace.textDocuments.map(document => { 37 | (!uri || uri.path === document.uri.path) && highlightProblems(document); 38 | }); 39 | } 40 | 41 | function highlightProblems(document:vscode.TextDocument) { 42 | if (path.extname(document.uri.path) !== '.php') { return; } 43 | const code = PHPParser.parseCode(document.getText()); 44 | const functions = PHPParser.findAllFunctions(code); 45 | 46 | MyDiagnostics.clearDiagnostics(document.uri); 47 | functions.forEach((func) => { 48 | func.type || MyDiagnostics.noReturnType(document.uri, getLastBracket(func)); 49 | }); 50 | MyDiagnostics.finishDiagnostics(document.uri); 51 | } 52 | 53 | function getLastBracket(func:PHPParser.NodeLayout): vscode.Range { 54 | // TODO - better to do text search for the last bracket between "p" and the start of the body 55 | let p, inc = 0; 56 | if (func.arguments && func.arguments.length) { 57 | p = func.arguments[func.arguments.length - 1].loc.end; 58 | } else { 59 | p = func.name.loc.end; 60 | inc = 1; 61 | } 62 | return new vscode.Range(p.line - 1, p.column + inc, p.line - 1, p.column + inc + 1); 63 | } --------------------------------------------------------------------------------