different attempt to parse using collapsable text

ranges
This commit is contained in:
Dave Holoway
2020-05-23 13:20:51 +01:00
parent bdc5b1d4cd
commit 23dc6d3871
18 changed files with 1904 additions and 345 deletions

View File

@@ -1,5 +1,5 @@
const ProblemSeverity = require('./problem-severity');
const Token = require('./token');
const { TextBlock } = require('./textblock');
/**
* @typedef {import('./import')} ImportDeclaration
@@ -11,123 +11,61 @@ const Token = require('./token');
class ParseProblem {
/**
* @param {Token|Token[]} token
* @param {TextBlock|TextBlock[]} token
* @param {string} message
* @param {Severity} severity
*/
constructor(token, message, severity) {
this.startIdx = (Array.isArray(token) ? token[0] : token).source_idx;
const lastToken = (Array.isArray(token) ? token[token.length - 1] : token);
this.endIdx = lastToken.source_idx + lastToken.text.length;
if (Array.isArray(token)) {
this.startIdx = token[0].range.start;
const lastToken = token[token.length - 1];
this.endIdx = lastToken.range.start + lastToken.range.length;
} else {
this.startIdx = token.range.start;
this.endIdx = this.startIdx + token.range.length;
}
this.message = message;
this.severity = severity;
}
/**
* @param {Modifier[]} mods
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static checkDuplicateModifiers(mods) {
const done = new Set();
const res = [];
for (let mod of mods) {
if (mod instanceof Token) {
if (done.has(mod.text)) {
res.push(new ParseProblem(mod, `Duplicate modifier: ${mod.text}`, ProblemSeverity.Error));
}
done.add(mod.text);
}
}
return res;
}
static checkConflictingModifiers(mods) {
const modmap = new Map();
let res = [];
mods.filter(m => m instanceof Token).forEach(m => modmap.set(m.text, m));
const names = [...modmap.keys()];
const visibilities = names.filter(m => /^(public|private|protected)$/.test(m));
if (visibilities.length > 1) {
const visnames = visibilities.map(m => `'${m}'`).join(', ').replace(/, (?='\w+'$)/, ' and ');
res = visibilities.map(m => new ParseProblem(modmap.get(m), `Conflicting modifiers: ${visnames}`, ProblemSeverity.Error));
}
if (names.includes('abstract')) {
if (names.includes('final')) {
res.push(new ParseProblem(modmap.get('final'), `Declarations cannot be both 'abstract' and 'final`, ProblemSeverity.Error));
}
if (names.includes('native')) {
res.push(new ParseProblem(modmap.get('native'), `Declarations cannot be both 'abstract' and 'native`, ProblemSeverity.Error));
}
}
return res;
static Error(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Error);
}
/**
* @param {Modifier[]} mods
* @param {'class'|'interface'|'enum'|'@interface'|'field'|'method'|'constructor'|'initializer'} decl_kind
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static checkAccessModifiers(mods, decl_kind) {
let valid_mods = /^$/;
switch (decl_kind) {
case 'class': valid_mods = /^(public|final|abstract|strictfp)$/; break;
case 'interface': valid_mods = /^(public|abstract|strictfp)$/; break;
case '@interface': valid_mods = /^(public)$/; break;
case 'enum': valid_mods = /^(public|final)$/; break;
case 'field': valid_mods = /^(public|private|protected|static|final|volatile|transient)$/; break;
case 'method': valid_mods = /^(public|private|protected|static|final|abstract|native|strictfp|synchronized)$/; break;
case 'constructor': valid_mods = /^(public|protected|native)$/; break;
case 'initializer': valid_mods = /^(static)$/; break;
}
const problems = [];
for (let mod of mods) {
if (mod instanceof Token) {
if (!valid_mods.test(mod.text)) {
problems.push(new ParseProblem(mod, `'${mod.text}' is not a valid modifier for ${decl_kind} type declarations`, ProblemSeverity.Warning));
}
const redundant = (mod.text === 'abstract' && decl_kind === 'interface')
|| (mod.text === 'final' && decl_kind === 'enum');
if (redundant) {
problems.push(new ParseProblem(mod, `'${mod.text}' is redundant for a ${decl_kind} declaration`, ProblemSeverity.Hint));
}
}
}
return problems;
static Warning(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Warning);
}
/**
* @param {PackageDeclaration|ImportDeclaration} o
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static checkSemicolon(o) {
if (!o.semicolon) {
const lastToken = o.lastToken();
return new ParseProblem(lastToken, 'Missing operator or semicolon', ProblemSeverity.Error);
}
static Information(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Information);
}
/**
* @param {Token[]} tokens
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static checkNonKeywordIdents(tokens) {
const res = [];
const KEYWORDS = /^(abstract|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|final|finally|for|goto|if|implements|import|interface|native|new|package|private|protected|public|return|static|strictfp|super|switch|synchronized|throw|throws|transient|try|volatile|while)$/;
const PRIMITIVE_TYPE_KEYWORDS = /^(int|boolean|byte|char|double|float|long|short|void)$/
const LITERAL_VALUE_KEYWORDS = /^(this|true|false|null)$/;
const OPERATOR_KEYWORDS = /^(instanceof)$/;
for (let token of tokens) {
let iskw = KEYWORDS.test(token.text) || PRIMITIVE_TYPE_KEYWORDS.test(token.text) || LITERAL_VALUE_KEYWORDS.test(token.text) || OPERATOR_KEYWORDS.test(token.text);
if (iskw) {
const problem = new ParseProblem(token, `'${token.text}' is a keyword and cannot be used as an identifier`, ProblemSeverity.Error);
res.push(problem);
}
}
return res;
static Hint(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Hint);
}
/**
* @param {Token} token
* @param {TextBlock|TextBlock[]} token
*/
static syntaxError(token) {
if (!token) return null;
return new ParseProblem(token, 'Unsupported, invalid or incomplete declaration', ProblemSeverity.Error);
return ParseProblem.Error(token, 'Unsupported, invalid or incomplete declaration');
}
}

View File

@@ -1,6 +1,4 @@
/**
* @typedef {import('./import')} ImportDeclaration
*/
const { ImportBlock } = require('../parser9');
/**
* Class representing a resolved import.
@@ -11,9 +9,10 @@
*/
class ResolvedImport {
/**
* @param {ImportDeclaration} import_decl
* @param {ImportBlock} import_decl
* @param {RegExpMatchArray} matches
* @param {'owner-package'|'import'|'implicit-import'} import_kind;
* @param {Map<string,import('../mti').Type>} typemap
* @param {'owner-package'|'import'|'implicit-import'} import_kind
*/
constructor(import_decl, matches, typemap, import_kind) {
/**

View File

@@ -58,6 +58,10 @@ class ResolvedType {
get label() {
return this.name + (this.typeargs ? `<${this.typeargs.map(arg => arg.label).join(',')}>` : '');
}
get rawlabel() {
return this.name;
}
}
/** @type {ResolvedType.TypePart[]} */
@@ -79,6 +83,13 @@ class ResolvedType {
*/
mtis = [];
/**
* @param {boolean} [isTypeArg]
*/
constructor(isTypeArg = false) {
this.isTypeArg = isTypeArg;
}
/**
* During parsing, add a new type part
* @param {string} [name]
@@ -97,9 +108,20 @@ class ResolvedType {
return this.parts.map(p => p.name).join('.');
}
get isPrimitive() {
if (this.arrdims > 0 || this.parts.length !== 1) {
return false;
}
return /^(int|boolean|char|void|byte|long|double|float|short)$/.test(this.parts[0].name);
}
get label() {
return this.parts.map(p => p.label).join('.') + '[]'.repeat(this.arrdims);
}
get rawlabel() {
return this.parts.map(p => p.rawlabel).join('.') + '[]'.repeat(this.arrdims);
}
};
module.exports = ResolvedType;

View File

@@ -0,0 +1,138 @@
class BlockRange {
get end() { return this.start + this.length }
get text() { return this.source.slice(this.start, this.end) }
/**
*
* @param {string} source
* @param {number} start
* @param {number} length
*/
constructor(source, start, length) {
this.source = source;
this.start = start;
this.length = length;
}
}
class TextBlock {
/**
* @param {BlockRange|TextBlockArray} range
* @param {string} simplified
*/
constructor(range, simplified) {
this.range = range;
this.simplified = simplified;
}
blockArray() {
return this.range instanceof TextBlockArray ? this.range : null;
}
/**
* Returns the length of the original source
* @returns {number}
*/
get length() {
return this.range.length;
}
/**
* @param {string} source
* @param {number} start
* @param {number} length
* @param {string} [simplified]
*/
static from(source, start, length, simplified) {
const range = new BlockRange(source, start, length);
return new TextBlock(range, simplified || range.text);
}
get source() { return this.toSource() }
/**
* @returns {string}
*/
toSource() {
return this.range instanceof BlockRange
? this.range.text
: this.range.toSource()
}
}
class TextBlockArray {
/**
* @param {string} id
* @param {TextBlock[]} [blocks]
*/
constructor(id, blocks = []) {
this.id = id;
this.blocks = blocks;
}
/**
* Returns the length of the original source
* @returns {number}
*/
get length() {
return this.blocks.reduce(((len,b) => len + b.length), 0);
}
get simplified() {
return this.blocks.map(tb => tb.simplified).join('');
}
/** @returns {number} */
get start() {
return this.blocks[0].range.start;
}
sourcemap() {
let idx = 0;
const parts = [];
/** @type {number[]} */
const map = this.blocks.reduce((arr,tb,i) => {
arr[idx] = i;
if (!tb) {
throw this.blocks;
}
parts.push(tb.simplified);
idx += tb.simplified.length;
return arr;
}, []);
map[idx] = this.blocks.length;
return {
simplified: parts.join(''),
map,
}
}
/**
* @param {string} id
* @param {number} start_block_idx
* @param {number} block_count
* @param {RegExpMatchArray} match
* @param {string} marker
* @param {*} [parseClass]
*/
shrink(id, start_block_idx, block_count, match, marker, parseClass) {
if (block_count <= 0) return;
const collapsed = new TextBlockArray(id, this.blocks.splice(start_block_idx, block_count, null));
const simplified = collapsed.source.replace(/./g, ' ').replace(/^./, marker);
return this.blocks[start_block_idx] = parseClass
? new parseClass(collapsed, simplified, match)
: new TextBlock(collapsed, simplified);
}
get source() { return this.toSource() }
toSource() {
return this.blocks.map(tb => tb.toSource()).join('');
}
}
module.exports = {
BlockRange,
TextBlock,
TextBlockArray,
}