| 1 |
/** |
| 2 |
* Based on work distributed under the BSD 3-Clause License (https://rawgit.com/glayzzle/codemirror-linter/master/LICENSE) |
| 3 |
*/ |
| 4 |
|
| 5 |
import Parser, { Block, Location, Node } from 'php-parser' |
| 6 |
import * as CodeMirror from 'codemirror' |
| 7 |
|
| 8 |
type Annotation = { message: string, severity: string, from: CodeMirror.Position, to: CodeMirror.Position } |
| 9 |
|
| 10 |
interface Identifier extends Node { |
| 11 |
name: string |
| 12 |
} |
| 13 |
|
| 14 |
interface Declaration extends Node { |
| 15 |
name: Identifier | string |
| 16 |
} |
| 17 |
|
| 18 |
class Linter { |
| 19 |
private readonly code: string |
| 20 |
|
| 21 |
private readonly function_names: Set<string> |
| 22 |
|
| 23 |
private readonly class_names: Set<string> |
| 24 |
|
| 25 |
public readonly annotations: Annotation[] |
| 26 |
|
| 27 |
/** |
| 28 |
* Constructor. |
| 29 |
* @param code |
| 30 |
*/ |
| 31 |
constructor(code: string) { |
| 32 |
this.code = code |
| 33 |
this.annotations = [] |
| 34 |
|
| 35 |
this.function_names = new Set() |
| 36 |
this.class_names = new Set() |
| 37 |
} |
| 38 |
|
| 39 |
/** |
| 40 |
* Lint the provided code. |
| 41 |
*/ |
| 42 |
lint() { |
| 43 |
const parser = new Parser({ |
| 44 |
parser: { |
| 45 |
suppressErrors: true, |
| 46 |
// eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 47 |
// @ts-ignore types file has not been updated to support this |
| 48 |
version: 800 |
| 49 |
}, |
| 50 |
ast: { |
| 51 |
withPositions: true |
| 52 |
} |
| 53 |
}) |
| 54 |
|
| 55 |
try { |
| 56 |
const ast = parser.parseEval(this.code) |
| 57 |
|
| 58 |
// Process any errors caught by the parser. |
| 59 |
if (ast.errors && 0 < ast.errors.length) { |
| 60 |
for (const error of ast.errors) { |
| 61 |
this.annotate(error.message as string, error.loc) |
| 62 |
} |
| 63 |
} |
| 64 |
|
| 65 |
// Visit each node to perform additional checks. |
| 66 |
this.visit(ast) |
| 67 |
|
| 68 |
} catch (error) { |
| 69 |
// eslint-disable-next-line no-console |
| 70 |
console.error(error) |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
/** |
| 75 |
* Visit nodes recursively. |
| 76 |
* @param node |
| 77 |
*/ |
| 78 |
visit(node: Node) { |
| 79 |
|
| 80 |
if (node.kind) { |
| 81 |
this.validate(node) |
| 82 |
} |
| 83 |
|
| 84 |
if ('children' in node) { |
| 85 |
const block = node as Block |
| 86 |
for (const child of block.children) { |
| 87 |
this.visit(child) |
| 88 |
} |
| 89 |
} |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* Check whether a given identifier has already been defined, creating an annotation if so. |
| 94 |
* @param identifier |
| 95 |
* @param registry |
| 96 |
* @param label |
| 97 |
*/ |
| 98 |
checkDuplicateIdentifier(identifier: Identifier, registry: Set<string>, label: string) { |
| 99 |
if (registry.has(identifier.name)) { |
| 100 |
this.annotate(`Cannot redeclare ${label} ${identifier.name}()`, identifier.loc) |
| 101 |
} else { |
| 102 |
registry.add(identifier.name) |
| 103 |
} |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* Perform additional validations on nodes. |
| 108 |
* @param node |
| 109 |
*/ |
| 110 |
validate(node: Node) { |
| 111 |
const decl = node as Declaration |
| 112 |
const ident = decl.name as Identifier |
| 113 |
|
| 114 |
if (!('name' in decl && 'name' in ident) || 'identifier' !== ident.kind) { |
| 115 |
return |
| 116 |
} |
| 117 |
|
| 118 |
if ('function' === node.kind) { |
| 119 |
this.checkDuplicateIdentifier(ident, this.function_names, 'function') |
| 120 |
|
| 121 |
} else if ('class' === node.kind) { |
| 122 |
this.checkDuplicateIdentifier(ident, this.class_names, 'class') |
| 123 |
} |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Create a lint annotation. |
| 128 |
* @param message |
| 129 |
* @param location |
| 130 |
* @param severity |
| 131 |
*/ |
| 132 |
annotate(message: string, location: Location, severity = 'error') { |
| 133 |
if (!location.start || !location.end) return |
| 134 |
|
| 135 |
const [start, end] = location.end.offset < location.start.offset ? |
| 136 |
[location.end, location.start] : |
| 137 |
[location.start, location.end] |
| 138 |
|
| 139 |
this.annotations.push({ |
| 140 |
message, |
| 141 |
severity, |
| 142 |
from: CodeMirror.Pos(start.line as number - 1, start.column as number), |
| 143 |
to: CodeMirror.Pos(end.line as number - 1, end.column as number) |
| 144 |
}) |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
CodeMirror.registerHelper('lint', 'php', (text: string) => { |
| 149 |
const linter = new Linter(text) |
| 150 |
linter.lint() |
| 151 |
|
| 152 |
return linter.annotations |
| 153 |
}) |
| 154 |
|