2 require_once __DIR__ .
'/../src/geshi.php';
31 self::TYPE_NOTICE =>
'[NOTICE] ',
32 self::TYPE_WARNING =>
'[WARNING]',
33 self::TYPE_ERROR =>
'[ERROR] ',
34 self::TYPE_FATAL =>
'[FATAL] ' 54 $this->issues =
array();
89 foreach ($this->issues as $issue) {
90 $string .= $this->severityNames[$issue[0]] .
' ' . $issue[1] .
"\n";
102 if (!is_file($this->
file)) {
103 $this->
issue(self::TYPE_FATAL,
'The path "' . $this->
file .
'" does not ressemble a regular file!');
106 if (!is_readable($this->
file)) {
107 $this->
issue(self::TYPE_FATAL,
'Cannot read file "' . $this->
file .
'"!');
110 $this->content = file_get_contents($this->
file);
118 if (preg_match(
'/\?>(?:\r?\n|\r(?!\n)){2,}\Z/', $this->content)) {
119 $this->
issue(self::TYPE_ERROR,
'Language file contains trailing empty lines at EOF!');
121 if (preg_match(
'/\?>(?:\r?\n|\r(?!\n))?\Z/', $this->content)) {
122 $this->
issue(self::TYPE_ERROR,
'Language file contains an PHP end marker at EOF!');
124 if (!preg_match(
'/(?:\r?\n|\r(?!\n))\Z/', $this->content)) {
125 $this->
issue(self::TYPE_ERROR,
'Language file contains no newline at EOF!');
127 if (preg_match(
'/(\r?\n|\r(?!\n))\\1\Z/', $this->content)) {
128 $this->
issue(self::TYPE_ERROR,
'Language file contains trailing empty line before EOF!');
130 if (preg_match(
'/[\x20\t]$/m', $this->content)) {
131 $this->
issue(self::TYPE_ERROR,
'Language file contains trailing whitespace at EOL!');
133 if (preg_match(
'/\t/', $this->content)) {
134 $this->
issue(self::TYPE_NOTICE,
'Language file contains unescaped tabulator chars (probably for indentation)!');
136 if (preg_match(
'/^(?: )*(?! )(?! \*) /m', $this->content)) {
137 $this->
issue(self::TYPE_NOTICE,
'Language file contains irregular indentation (other than 4 spaces per indentation level)!');
139 if (preg_match(
'/\015\012/s', $this->content)) {
140 $this->
issue(self::TYPE_ERROR,
'Language file contains DOS line endings!');
141 }
else if (preg_match(
'/\015/s', $this->content)) {
142 $this->
issue(self::TYPE_ERROR,
'Language file contains MAC line endings!');
151 if (!preg_match(
'/\/\*\*\**\s(.*?)(?:\s*\*\/)/s', $this->content,
$m)) {
152 $this->
issue(self::TYPE_ERROR,
'Language file does not have an initial comment block!');
157 if (!preg_match(
'/Author: +\S+/',
$comment)) {
158 $this->
issue(self::TYPE_WARNING,
'Language file does not contain a specification of an author!');
160 if (!preg_match(
'/Copyright: +\S+/',
$comment)) {
161 $this->
issue(self::TYPE_WARNING,
'Language file does not contain a specification of the copyright!');
163 if (!preg_match(
'/Release Version: +\d+\.\d+\.\d+\.\d+/',
$comment)) {
164 $this->
issue(self::TYPE_WARNING,
'Language file does not contain a specification of the release version!');
166 if (!preg_match(
'/Date Started: +\S+/s',
$comment)) {
167 $this->
issue(self::TYPE_WARNING,
'Language file does not contain a specification of the date it was started!');
169 if (!preg_match(
'/This file is part of GeSHi\./',
$comment)) {
170 $this->
issue(self::TYPE_WARNING,
'Language file does not state that it belongs to GeSHi!');
172 if (!preg_match(
'/\S+ language file for GeSHi\./',
$comment)) {
173 $this->
issue(self::TYPE_WARNING,
'Language file does not state that it is a language file for GeSHi!');
175 if (!preg_match(
'/GNU General Public License/',
$comment)) {
176 $this->
issue(self::TYPE_WARNING,
'Language file does not state that it is provided under the terms of the GNU GPL!');
192 $this->
issue(self::TYPE_FATAL,
'Language file does not contain a $language_data structure to check!');
196 $this->
issue(self::TYPE_FATAL,
'Language file contains a $language_data structure which is not an array!');
202 $defined = get_defined_vars();
204 $this->
issue(self::TYPE_ERROR,
'Language file seems to define other variables than $language_data!');
220 $types = explode(
'|',
$type);
222 if (!isset($this->langdata[
$name])) {
226 $this->
issue(self::TYPE_ERROR,
"Language file contains no \$language_data['$name'] specification!");
230 if (!in_array(gettype($this->langdata[$name]), $types)) {
231 $this->
issue(self::TYPE_ERROR,
"Language file contains a \$language_data['$name'] specification which is not a $type!");
259 if (1 < strlen($this->langdata[
'ESCAPE_CHAR'])) {
260 $this->
issue(self::TYPE_ERROR,
'Language file contains a $language_data[\'ESCAPE_CHAR\'] specification is not empty or exactly one char!');
269 $this->
issue(self::TYPE_ERROR,
'Language file contains a $language_data[\'CASE_KEYWORDS\'] specification which is neither of GESHI_CAPS_NO_CHANGE, GESHI_CAPS_LOWER nor GESHI_CAPS_UPPER!');
274 foreach ($this->langdata[
'KEYWORDS'] as $kw_key => $kw_value) {
275 if (!is_integer($kw_key)) {
276 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$kw_key' in \$language_data['KEYWORDS'] that is not integer!");
277 } elseif (!is_array($kw_value)) {
278 $this->
issue(self::TYPE_ERROR,
"Language file contains a \$language_data['KEYWORDS']['$kw_value'] structure which is not an array!");
284 foreach ($this->langdata[
'CASE_SENSITIVE'] as $cs_key => $cs_value) {
285 if (!is_integer($cs_key)) {
286 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$cs_key' in \$language_data['CASE_SENSITIVE'] that is not integer!");
287 } elseif (!is_bool($cs_value)) {
288 $this->
issue(self::TYPE_ERROR,
"Language file contains a Case Sensitivity specification for \$language_data['CASE_SENSITIVE']['$cs_value'] which is not a boolean!");
294 foreach ($this->langdata[
'URLS'] as $url_key => $url_value) {
295 if (!is_integer($url_key)) {
296 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$url_key' in \$language_data['URLS'] that is not integer!");
297 } elseif (!is_string($url_value)) {
298 $this->
issue(self::TYPE_ERROR,
"Language file contains a Documentation URL specification for \$language_data['URLS']['$url_value'] which is not a string!");
299 } elseif (preg_match(
'#&([^;]*(=|$))#U', $url_value)) {
300 $this->
issue(self::TYPE_ERROR,
"Language file contains unescaped ampersands (&) in \$language_data['URLS']!");
306 if (
false !== $this->langdata[
'OOLANG'] &&
307 true !== $this->langdata[
'OOLANG'] &&
308 2 !== $this->langdata[
'OOLANG']
310 $this->
issue(self::TYPE_ERROR,
'Language file contains a $language_data[\'OOLANG\'] specification which is neither of false, true or 2!');
314 if ($this->
ensureKeyType(
'STRICT_MODE_APPLIES',
'integer')) {
315 if (
GESHI_MAYBE != $this->langdata[
'STRICT_MODE_APPLIES'] &&
316 GESHI_ALWAYS != $this->langdata[
'STRICT_MODE_APPLIES'] &&
317 GESHI_NEVER != $this->langdata[
'STRICT_MODE_APPLIES']
319 $this->
issue(self::TYPE_ERROR,
'Language file contains a $language_data[\'STRICT_MODE_APPLIES\'] specification which is neither of GESHI_MAYBE, GESHI_ALWAYS nor GESHI_NEVER!');
324 if (1 > $this->langdata[
'TAB_WIDTH']) {
325 $this->
issue(self::TYPE_ERROR,
'Language file contains a $language_data[\'TAB_WIDTH\'] specification which is less than 1!');
330 $style_arrays =
array(
'KEYWORDS',
'COMMENTS',
'ESCAPE_CHAR',
331 'BRACKETS',
'STRINGS',
'NUMBERS',
'METHODS',
'SYMBOLS',
332 'REGEXPS',
'SCRIPT');
333 foreach ($style_arrays as $style_kind) {
334 if (!isset($this->langdata[
'STYLES'][$style_kind])) {
335 $this->
issue(self::TYPE_ERROR,
"Language file contains no \$language_data['STYLES']['$style_kind'] structure to check!");
336 } elseif (!is_array($this->langdata[
'STYLES'][$style_kind])) {
337 $this->
issue(self::TYPE_ERROR,
"Language file contains a \$language_data['STYLES\']['$style_kind'] structure which is not an array!");
339 foreach ($this->langdata[
'STYLES'][$style_kind] as $sk_key => $sk_value) {
340 if (!is_int($sk_key) && (
'COMMENTS' != $style_kind &&
'MULTI' != $sk_key)
341 && !((
'STRINGS' == $style_kind ||
'ESCAPE_CHAR' == $style_kind) &&
'HARD' == $sk_key)
343 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$sk_key' in \$language_data['STYLES']['$style_kind'] that is not integer!");
344 } elseif (!is_string($sk_value)) {
345 $this->
issue(self::TYPE_WARNING,
"Language file contains a CSS specification for \$language_data['STYLES']['$style_kind'][$key] which is not a string!");
360 foreach ($this->langdata[
'KEYWORDS'] as
$key => $keywords) {
361 if (!isset($this->langdata[
'CASE_SENSITIVE'][
$key])) {
362 $this->
issue(self::TYPE_ERROR,
"Language file contains no \$language_data['CASE_SENSITIVE'] specification for keyword group $key!");
364 if (!isset($this->langdata[
'URLS'][$key])) {
365 $this->
issue(self::TYPE_ERROR,
"Language file contains no \$language_data['URLS'] specification for keyword group $key!");
367 if (empty($keywords)) {
368 $this->
issue(self::TYPE_WARNING,
"Language file contains an empty keyword list in \$language_data['KEYWORDS'] for group $key!");
370 foreach ($keywords as
$id => $kw) {
371 if (!is_string($kw)) {
372 $this->
issue(self::TYPE_WARNING,
"Language file contains an non-string entry at \$language_data['KEYWORDS'][$key][$id]!");
373 } elseif (!strlen($kw)) {
374 $this->
issue(self::TYPE_ERROR,
"Language file contains an empty string entry at \$language_data['KEYWORDS'][$key][$id]!");
375 } elseif (preg_match(
'/^([\(\)\{\}\[\]\^=.,:;\-+\*\/%\$\"\'\?]|&[\w#]\w*;)+$/i', $kw)) {
376 $this->
issue(self::TYPE_NOTICE,
"Language file contains an keyword ('$kw') at \$language_data['KEYWORDS'][$key][$id] which seems to be better suited for the symbols section!");
379 if (isset($this->langdata[
'CASE_SENSITIVE'][$key]) && !$this->langdata[
'CASE_SENSITIVE'][$key]) {
380 array_walk($keywords,
array($this,
'strtolower'));
382 if (count($keywords) != count(array_unique($keywords))) {
383 $kw_diffs = array_count_values($keywords);
384 foreach ($kw_diffs as $kw => $kw_count) {
386 $this->
issue(self::TYPE_WARNING,
"Language file contains per-group duplicate keyword '$kw' in \$language_data['KEYWORDS'][$key]!");
392 $disallowed_before =
'(?<![a-zA-Z0-9\$_\|\#;>|^&';
393 $disallowed_after =
'(?![a-zA-Z0-9_\|%\\-&;';
395 foreach ($this->langdata[
'KEYWORDS'] as
$key => $keywords) {
396 foreach ($this->langdata[
'KEYWORDS'] as $key2 => $keywords2) {
400 $kw_diffs = array_intersect($keywords, $keywords2);
401 foreach ($kw_diffs as $kw) {
402 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'])) {
404 $g1_pre = $disallowed_before;
405 $g2_pre = $disallowed_before;
406 $g1_post = $disallowed_after;
407 $g2_post = $disallowed_after;
408 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_BEFORE'])) {
409 $g1_pre = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_BEFORE'];
410 $g2_pre = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_BEFORE'];
412 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_AFTER'])) {
413 $g1_post = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_AFTER'];
414 $g2_post = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
'DISALLOWED_AFTER'];
417 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
$key][
'DISALLOWED_BEFORE'])) {
418 $g1_pre = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
$key][
'DISALLOWED_BEFORE'];
420 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][$key][
'DISALLOWED_AFTER'])) {
421 $g1_post = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][
$key][
'DISALLOWED_AFTER'];
424 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][$key2][
'DISALLOWED_BEFORE'])) {
425 $g2_pre = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][$key2][
'DISALLOWED_BEFORE'];
427 if (isset($this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][$key2][
'DISALLOWED_AFTER'])) {
428 $g2_post = $this->langdata[
'PARSER_CONTROL'][
'KEYWORDS'][$key2][
'DISALLOWED_AFTER'];
431 if ($g1_pre != $g2_pre || $g1_post != $g2_post) {
435 $this->
issue(self::TYPE_WARNING,
"Language file contains cross-group duplicate keyword '$kw' in \$language_data['KEYWORDS'][$key] and \$language_data['KEYWORDS'][$key2]!");
439 foreach ($this->langdata[
'CASE_SENSITIVE'] as
$key => $keywords) {
441 $this->
issue(self::TYPE_WARNING,
"Language file contains an superfluous \$language_data['CASE_SENSITIVE'] specification for non-existing keyword group $key!");
444 foreach ($this->langdata[
'URLS'] as
$key => $keywords) {
445 if (!isset($this->langdata[
'KEYWORDS'][
$key])) {
446 $this->
issue(self::TYPE_WARNING,
"Language file contains an superfluous \$language_data['URLS'] specification for non-existing keyword group $key!");
449 foreach ($this->langdata[
'STYLES'][
'KEYWORDS'] as
$key => $keywords) {
450 if (!isset($this->langdata[
'KEYWORDS'][
$key])) {
451 $this->
issue(self::TYPE_WARNING,
"Language file contains an superfluous \$language_data['STYLES']['KEYWORDS'] specification for non-existing keyword group $key!");
455 foreach ($this->langdata[
'COMMENT_SINGLE'] as $ck => $cv) {
457 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$ck' in \$language_data['COMMENT_SINGLE'] that is not integer!");
459 if (!is_string($cv)) {
460 $this->
issue(self::TYPE_WARNING,
"Language file contains an non-string entry at \$language_data['COMMENT_SINGLE'][$ck]!");
462 if (!isset($this->langdata[
'STYLES'][
'COMMENTS'][$ck])) {
463 $this->
issue(self::TYPE_WARNING,
"Language file contains no \$language_data['STYLES']['COMMENTS'] specification for comment group $ck!");
466 if (isset($this->langdata[
'COMMENT_REGEXP'])) {
467 foreach ($this->langdata[
'COMMENT_REGEXP'] as $ck => $cv) {
469 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$ck' in \$language_data['COMMENT_REGEXP'] that is not integer!");
471 if (!is_string($cv)) {
472 $this->
issue(self::TYPE_WARNING,
"Language file contains an non-string entry at \$language_data['COMMENT_REGEXP'][$ck]!");
474 if (!isset($this->langdata[
'STYLES'][
'COMMENTS'][$ck])) {
475 $this->
issue(self::TYPE_WARNING,
"Language file contains no \$language_data['STYLES']['COMMENTS'] specification for comment group $ck!");
479 foreach ($this->langdata[
'STYLES'][
'COMMENTS'] as $ck => $cv) {
480 if ($ck !=
'MULTI' && !isset($this->langdata[
'COMMENT_SINGLE'][$ck]) &&
481 !isset($this->langdata[
'COMMENT_REGEXP'][$ck])
483 $this->
issue(self::TYPE_NOTICE,
"Language file contains an superfluous \$language_data['STYLES']['COMMENTS'] specification for Single Line or Regular-Expression Comment key $ck!");
486 if (isset($this->langdata[
'STYLES'][
'STRINGS'][
'HARD'])) {
487 if (empty($this->langdata[
'HARDQUOTE'])) {
488 $this->
issue(self::TYPE_NOTICE,
"Language file contains superfluous \$language_data['STYLES']['STRINGS'] specification for key 'HARD', but no 'HARDQUOTE's are defined!");
490 unset($this->langdata[
'STYLES'][
'STRINGS'][
'HARD']);
492 foreach ($this->langdata[
'STYLES'][
'STRINGS'] as $sk => $sv) {
493 if ($sk && !isset($this->langdata[
'QUOTEMARKS'][$sk])) {
494 $this->
issue(self::TYPE_NOTICE,
"Language file contains an superfluous \$language_data['STYLES']['STRINGS'] specification for non-existing quotemark key $sk!");
498 foreach ($this->langdata[
'REGEXPS'] as $rk => $rv) {
500 $this->
issue(self::TYPE_WARNING,
"Language file contains an key '$rk' in \$language_data['REGEXPS'] that is not integer!");
502 if (is_string($rv)) {
505 $this->
issue(self::TYPE_WARNING,
"Language file contains an empty regular expression at \$language_data['REGEXPS'][$rk]!");
507 if (preg_match(
"/(?<!\\\\)\//s", $rv)) {
508 $this->
issue(self::TYPE_WARNING,
"Language file contains a regular expression with an unmasked / character at \$language_data['REGEXPS'][$rk]!");
509 } elseif (preg_match(
"/(?<!<)(\\\\\\\\)*\\\\\|(?!>)/s", $rv)) {
510 $this->
issue(self::TYPE_WARNING,
"Language file contains a regular expression with an unescaped match for a pipe character '|' which needs escaping as '<PIPE>' instead at \$language_data['REGEXPS'][$rk]!");
513 } elseif (is_array($rv)) {
515 $this->
issue(self::TYPE_ERROR,
"Language file contains no GESHI_SEARCH entry in extended regular expression at \$language_data['REGEXPS'][$rk]!");
516 } elseif (!is_string($rv[GESHI_SEARCH])) {
517 $this->
issue(self::TYPE_ERROR,
"Language file contains a GESHI_SEARCH entry in extended regular expression at \$language_data['REGEXPS'][$rk] which is not a string!");
519 if (preg_match(
"/(?<!\\\\)\//s", $rv[GESHI_SEARCH])) {
520 $this->
issue(self::TYPE_WARNING,
"Language file contains a regular expression with an unmasked / character at \$language_data['REGEXPS'][$rk]!");
521 } elseif (preg_match(
"/(?<!<)(\\\\\\\\)*\\\\\|(?!>)/s", $rv[GESHI_SEARCH])) {
522 $this->
issue(self::TYPE_WARNING,
"Language file contains a regular expression with an unescaped match for a pipe character '|' which needs escaping as '<PIPE>' instead at \$language_data['REGEXPS'][$rk]!");
526 $this->
issue(self::TYPE_WARNING,
"Language file contains no GESHI_REPLACE entry in extended regular expression at \$language_data['REGEXPS'][$rk]!");
527 } elseif (!is_string($rv[GESHI_REPLACE])) {
528 $this->
issue(self::TYPE_ERROR,
"Language file contains a GESHI_REPLACE entry in extended regular expression at \$language_data['REGEXPS'][$rk] which is not a string!");
531 $this->
issue(self::TYPE_WARNING,
"Language file contains no GESHI_MODIFIERS entry in extended regular expression at \$language_data['REGEXPS'][$rk]!");
532 } elseif (!is_string($rv[GESHI_MODIFIERS])) {
533 $this->
issue(self::TYPE_ERROR,
"Language file contains a GESHI_MODIFIERS entry in extended regular expression at \$language_data['REGEXPS'][$rk] which is not a string!");
536 $this->
issue(self::TYPE_WARNING,
"Language file contains no GESHI_BEFORE entry in extended regular expression at \$language_data['REGEXPS'][$rk]!");
537 } elseif (!is_string($rv[GESHI_BEFORE])) {
538 $this->
issue(self::TYPE_ERROR,
"Language file contains a GESHI_BEFORE entry in extended regular expression at \$language_data['REGEXPS'][$rk] which is not a string!");
541 $this->
issue(self::TYPE_WARNING,
"Language file contains no GESHI_AFTER entry in extended regular expression at \$language_data['REGEXPS'][$rk]!");
542 } elseif (!is_string($rv[GESHI_AFTER])) {
543 $this->
issue(self::TYPE_ERROR,
"Language file contains a GESHI_AFTER entry in extended regular expression at \$language_data['REGEXPS'][$rk] which is not a string!");
546 $this->
issue(self::TYPE_WARNING,
"Language file contains an non-string and non-array entry at \$language_data['REGEXPS'][$rk]!");
548 if (!isset($this->langdata[
'STYLES'][
'REGEXPS'][$rk])) {
549 $this->
issue(self::TYPE_WARNING,
"Language file contains no \$language_data['STYLES']['REGEXPS'] specification for regexp group $rk!");
552 foreach ($this->langdata[
'STYLES'][
'REGEXPS'] as $rk => $rv) {
553 if (!isset($this->langdata[
'REGEXPS'][$rk])) {
554 $this->
issue(self::TYPE_NOTICE,
"Language file contains an superfluous \$language_data['STYLES']['REGEXPS'] specification for regexp key $rk!");
572 if (
$type == self::TYPE_FATAL) {
loadLanguageData()
Load the language data.
const GESHI_MODIFIERS
The key of the regex array defining any modifiers to the regular expression.
getIssuesAsString()
Get all found issues as formatted list.
if(!array_key_exists('StateId', $_REQUEST)) $id
const GESHI_BEFORE
The key of the regex array defining what bracket group in a matched search to put before the replacem...
__construct($file)
LangCheck constructor.
issue($type, $msg)
log an issue
const GESHI_COMMENTS
Used in language files to mark comments.
checkGeneral()
Check some general file properties.
const GESHI_REPLACE
The key of the regex array defining what bracket group in a matched search to use as a replacement...
checkMainKeys()
Check the major keys in the language array.
loadFile()
Load the file content.
const GESHI_CAPS_UPPER
Uppercase keywords found.
ensureKeyType($name, $type='array', $optional=false)
Check that the given key exists and has the correct type.
getIssues()
Get all found issues as (severity, message) tuple.
checkKeyContents()
Check the keywords are sane.
Reload workbook from saved file
const GESHI_SEARCH
The key of the regex array defining what to search for.
Create styles array
The data for the language used.
const GESHI_CAPS_LOWER
Leave keywords found as the case that they are.
const GESHI_MAYBE
Strict mode might apply, and can be enabled or disabled by GeSHi->enable_strict_mode().
const GESHI_NEVER
#+ private
const GESHI_CAPS_NO_CHANGE
Lowercase keywords found.
checkComment()
Check that the needed information is in the initial file comment.
const GESHI_ALWAYS
Strict mode always applies.
runChecks()
Run all the checks on the file.
const GESHI_AFTER
The key of the regex array defining what bracket group in a matched search to put after the replaceme...