Better JS language integration for Pre-Request scripts and Test scripts (#1422)
* Add tern as dependency * Add build rule to transpile mjs * Initial implementation of the auto complete engine * Separate out the tern server code to separate file * Added extra type defs for tern server * Boost the pw completion result to the top of the list * Added acorn and acorn-walk as dependency * Semantic linting powered by tern * Fix DeepCode warnings for js-editor * Remove unused registerLint tern extension Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -21,7 +21,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import ace from "ace-builds"
|
import ace from "ace-builds"
|
||||||
import "ace-builds/webpack-resolver"
|
import "ace-builds/webpack-resolver"
|
||||||
|
import "ace-builds/src-noconflict/ext-language_tools"
|
||||||
|
import "ace-builds/src-noconflict/mode-graphqlschema"
|
||||||
import debounce from "~/helpers/utils/debounce"
|
import debounce from "~/helpers/utils/debounce"
|
||||||
|
import {
|
||||||
|
getPreRequestScriptCompletions,
|
||||||
|
getTestScriptCompletions,
|
||||||
|
performPreRequestLinting,
|
||||||
|
} from "~/helpers/tern"
|
||||||
|
|
||||||
import * as esprima from "esprima"
|
import * as esprima from "esprima"
|
||||||
|
|
||||||
@@ -44,6 +51,11 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
completeMode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -65,7 +77,12 @@ export default {
|
|||||||
theme() {
|
theme() {
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||||
this.$nextTick().then(() => {
|
this.$nextTick()
|
||||||
|
.then(() => {
|
||||||
|
this.initialized = true
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// nextTick shouldn't really ever throw but still
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -76,17 +93,62 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const langTools = ace.require("ace/ext/language_tools")
|
||||||
|
|
||||||
const editor = ace.edit(this.$refs.editor, {
|
const editor = ace.edit(this.$refs.editor, {
|
||||||
mode: `ace/mode/javascript`,
|
mode: `ace/mode/javascript`,
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
...this.options,
|
...this.options,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||||
this.$nextTick().then(() => {
|
this.$nextTick()
|
||||||
|
.then(() => {
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// nextTIck shouldn't really ever throw but still
|
||||||
|
this.initalized = true
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const completer = {
|
||||||
|
getCompletions: (editor, _session, { row, column }, _prefix, callback) => {
|
||||||
|
if (this.completeMode === "pre") {
|
||||||
|
getPreRequestScriptCompletions(editor.getValue(), row, column)
|
||||||
|
.then((res) => {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
res.completions.map((r, index, arr) => ({
|
||||||
|
name: r.name,
|
||||||
|
value: r.name,
|
||||||
|
score: (arr.length - index) / arr.length,
|
||||||
|
meta: r.type,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(() => callback(null, []))
|
||||||
|
} else if (this.completeMode === "test") {
|
||||||
|
getTestScriptCompletions(editor.getValue(), row, column)
|
||||||
|
.then((res) => {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
res.completions.map((r, index, arr) => ({
|
||||||
|
name: r.name,
|
||||||
|
value: r.name,
|
||||||
|
score: (arr.length - index) / arr.length,
|
||||||
|
meta: r.type,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(() => callback(null, []))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.completers = [completer]
|
||||||
|
|
||||||
if (this.value) editor.setValue(this.value, 1)
|
if (this.value) editor.setValue(this.value, 1)
|
||||||
|
|
||||||
@@ -115,17 +177,30 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
provideLinting: debounce(function (code) {
|
provideLinting: debounce(function (code) {
|
||||||
|
let results = []
|
||||||
|
|
||||||
|
performPreRequestLinting(code)
|
||||||
|
.then((semanticLints) => {
|
||||||
|
results = results.concat(
|
||||||
|
semanticLints.map((lint) => ({
|
||||||
|
row: lint.from.line,
|
||||||
|
column: lint.from.ch,
|
||||||
|
text: `[semantic] ${lint.message}`,
|
||||||
|
type: "error",
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = esprima.parseScript(code, { tolerant: true })
|
const res = esprima.parseScript(code, { tolerant: true })
|
||||||
if (res.errors && res.errors.length > 0) {
|
if (res.errors && res.errors.length > 0) {
|
||||||
this.editor.session.setAnnotations(
|
results = results.concat(
|
||||||
res.errors.map((err) => {
|
res.errors.map((err) => {
|
||||||
const pos = this.editor.session.getDocument().indexToPosition(err.index, 0)
|
const pos = this.editor.session.getDocument().indexToPosition(err.index, 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
row: pos.row,
|
row: pos.row,
|
||||||
column: pos.column,
|
column: pos.column,
|
||||||
text: err.description,
|
text: `[syntax] ${err.description}`,
|
||||||
type: "error",
|
type: "error",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -133,15 +208,49 @@ export default {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const pos = this.editor.session.getDocument().indexToPosition(e.index, 0)
|
const pos = this.editor.session.getDocument().indexToPosition(e.index, 0)
|
||||||
this.editor.session.setAnnotations([
|
results = results.concat([
|
||||||
{
|
{
|
||||||
row: pos.row,
|
row: pos.row,
|
||||||
column: pos.column,
|
column: pos.column,
|
||||||
text: e.description,
|
text: `[syntax] ${e.description}`,
|
||||||
type: "error",
|
type: "error",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.editor.session.setAnnotations(results)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
try {
|
||||||
|
const res = esprima.parseScript(code, { tolerant: true })
|
||||||
|
if (res.errors && res.errors.length > 0) {
|
||||||
|
results = results.concat(
|
||||||
|
res.errors.map((err) => {
|
||||||
|
const pos = this.editor.session.getDocument().indexToPosition(err.index, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
row: pos.row,
|
||||||
|
column: pos.column,
|
||||||
|
text: `[syntax] ${err.description}`,
|
||||||
|
type: "error",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const pos = this.editor.session.getDocument().indexToPosition(e.index, 0)
|
||||||
|
results = results.concat([
|
||||||
|
{
|
||||||
|
row: pos.row,
|
||||||
|
column: pos.column,
|
||||||
|
text: `[syntax] ${e.description}`,
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.session.setAnnotations(results)
|
||||||
|
})
|
||||||
}, 2000),
|
}, 2000),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
119
helpers/tern.js
Normal file
119
helpers/tern.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import tern from "tern"
|
||||||
|
|
||||||
|
import ECMA_DEF from "~/helpers/terndoc/ecma.json"
|
||||||
|
import PW_PRE_DEF from "~/helpers/terndoc/pw-pre.json"
|
||||||
|
import PW_TEST_DEF from "~/helpers/terndoc/pw-test.json"
|
||||||
|
import PW_EXTRAS_DEF from "~/helpers/terndoc/pw-extras.json"
|
||||||
|
|
||||||
|
import { registerTernLinter } from "./ternlint"
|
||||||
|
|
||||||
|
const server = new tern.Server({
|
||||||
|
defs: [ECMA_DEF, PW_EXTRAS_DEF],
|
||||||
|
plugins: {
|
||||||
|
lint: {
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
registerTernLinter()
|
||||||
|
|
||||||
|
function performLinting(code) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.request(
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
type: "lint",
|
||||||
|
file: "doc",
|
||||||
|
lineCharPositions: true,
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
type: "full",
|
||||||
|
name: "doc",
|
||||||
|
text: code,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (!err) resolve(res.messages)
|
||||||
|
else reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function performPreRequestLinting(code) {
|
||||||
|
server.deleteDefs("pw-test")
|
||||||
|
server.deleteDefs("pw-pre")
|
||||||
|
server.addDefs(PW_PRE_DEF)
|
||||||
|
return performLinting(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function performTestLinting(code) {
|
||||||
|
server.deleteDefs("pw-test")
|
||||||
|
server.deleteDefs("pw-pre")
|
||||||
|
server.addDefs(PW_TEST_DEF)
|
||||||
|
return performLinting(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function postProcessCompletionResult(res) {
|
||||||
|
if (res.completions) {
|
||||||
|
const index = res.completions.findIndex((el) => el.name === "pw")
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
const result = res.completions[index]
|
||||||
|
|
||||||
|
res.completions.splice(index, 1)
|
||||||
|
res.completions.splice(0, 0, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function performCompletion(code, row, col) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.request(
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
type: "completions",
|
||||||
|
file: "doc",
|
||||||
|
end: {
|
||||||
|
line: row,
|
||||||
|
ch: col,
|
||||||
|
},
|
||||||
|
guess: false,
|
||||||
|
types: true,
|
||||||
|
includeKeywords: true,
|
||||||
|
inLiteral: false,
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
type: "full",
|
||||||
|
name: "doc",
|
||||||
|
text: code,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(err, res) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(postProcessCompletionResult(res))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreRequestScriptCompletions(code, row, col) {
|
||||||
|
server.deleteDefs("pw-test")
|
||||||
|
server.deleteDefs("pw-pre")
|
||||||
|
server.addDefs(PW_PRE_DEF)
|
||||||
|
return performCompletion(code, row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTestScriptCompletions(code, row, col) {
|
||||||
|
server.deleteDefs("pw-test")
|
||||||
|
server.deleteDefs("pw-pre")
|
||||||
|
server.addDefs(PW_TEST_DEF)
|
||||||
|
return performCompletion(code, row, col)
|
||||||
|
}
|
||||||
1284
helpers/terndoc/ecma.json
Normal file
1284
helpers/terndoc/ecma.json
Normal file
File diff suppressed because it is too large
Load Diff
51
helpers/terndoc/pw-extras.json
Normal file
51
helpers/terndoc/pw-extras.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"!name": "pw-extra",
|
||||||
|
"console": {
|
||||||
|
"assert": {
|
||||||
|
"!type": "fn(assertion: bool, text: string)"
|
||||||
|
},
|
||||||
|
"clear": {
|
||||||
|
"!type": "fn()"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"!type": "fn(label?: string)"
|
||||||
|
},
|
||||||
|
"debug": "console.log",
|
||||||
|
"dir": {
|
||||||
|
"!type": "fn(object: ?)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"!type": "fn(...msg: ?)"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"!type": "fn(label?: string)"
|
||||||
|
},
|
||||||
|
"groupCollapsed": {
|
||||||
|
"!type": "fn(label?: string)"
|
||||||
|
},
|
||||||
|
"groupEnd": {
|
||||||
|
"!type": "fn()"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"!type": "fn(...msg: ?)"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"!type": "fn(...msg: ?)"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"!type": "fn(data: []|?, columns?: [])"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"!type": "fn(label: string)"
|
||||||
|
},
|
||||||
|
"timeEnd": {
|
||||||
|
"!type": "fn(label: string)"
|
||||||
|
},
|
||||||
|
"trace": {
|
||||||
|
"!type": "fn()"
|
||||||
|
},
|
||||||
|
"warn": {
|
||||||
|
"!type": "fn(...msg: ?)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
helpers/terndoc/pw-pre.json
Normal file
8
helpers/terndoc/pw-pre.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"!name": "pw-pre",
|
||||||
|
"pw": {
|
||||||
|
"env": {
|
||||||
|
"set": "fn(key: string, value: string)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
helpers/terndoc/pw-test.json
Normal file
24
helpers/terndoc/pw-test.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"!name": "pw-test",
|
||||||
|
"!define": {
|
||||||
|
"Expectation": {
|
||||||
|
"not": "Expectation",
|
||||||
|
"toBe": "fn(value: ?)",
|
||||||
|
"toBeLevel2xx": "fn()",
|
||||||
|
"toBeLevel3xx": "fn()",
|
||||||
|
"toBeLevel4xx": "fn()",
|
||||||
|
"toBeLevel5xx": "fn()",
|
||||||
|
"toBeType": "fn(type: string)",
|
||||||
|
"toHaveLength": "fn(length: number)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pw": {
|
||||||
|
"expect": "fn(value: ?) -> Expectation",
|
||||||
|
"response": {
|
||||||
|
"status": "number",
|
||||||
|
"headers": "?",
|
||||||
|
"body": "?"
|
||||||
|
},
|
||||||
|
"test": "fn(name: string, func: fn())"
|
||||||
|
}
|
||||||
|
}
|
||||||
672
helpers/ternlint.js
Normal file
672
helpers/ternlint.js
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
import infer from "tern/lib/infer"
|
||||||
|
import tern from "tern/lib/tern"
|
||||||
|
|
||||||
|
const walk = require("acorn-walk")
|
||||||
|
|
||||||
|
var defaultRules = {
|
||||||
|
UnknownProperty: { severity: "warning" },
|
||||||
|
UnknownIdentifier: { severity: "warning" },
|
||||||
|
NotAFunction: { severity: "error" },
|
||||||
|
InvalidArgument: { severity: "error" },
|
||||||
|
UnusedVariable: { severity: "warning" },
|
||||||
|
UnknownModule: { severity: "error" },
|
||||||
|
MixedReturnTypes: { severity: "warning" },
|
||||||
|
ObjectLiteral: { severity: "error" },
|
||||||
|
TypeMismatch: { severity: "warning" },
|
||||||
|
Array: { severity: "error" },
|
||||||
|
ES6Modules: { severity: "error" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeVisitors(server, query, file, messages) {
|
||||||
|
function addMessage(node, msg, severity) {
|
||||||
|
var error = makeError(node, msg, severity)
|
||||||
|
messages.push(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeError(node, msg, severity) {
|
||||||
|
var pos = getPosition(node)
|
||||||
|
var error = {
|
||||||
|
message: msg,
|
||||||
|
from: tern.outputPos(query, file, pos.start),
|
||||||
|
to: tern.outputPos(query, file, pos.end),
|
||||||
|
severity: severity,
|
||||||
|
}
|
||||||
|
if (query.lineNumber) {
|
||||||
|
error.lineNumber = query.lineCharPositions
|
||||||
|
? error.from.line
|
||||||
|
: tern.outputPos({ lineCharPositions: true }, file, pos.start).line
|
||||||
|
}
|
||||||
|
if (!query.groupByFiles) error.file = file.name
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeName(node) {
|
||||||
|
if (node.callee) {
|
||||||
|
// This is a CallExpression node.
|
||||||
|
// We get the position of the function name.
|
||||||
|
return getNodeName(node.callee)
|
||||||
|
} else if (node.property) {
|
||||||
|
// This is a MemberExpression node.
|
||||||
|
// We get the name of the property.
|
||||||
|
return node.property.name
|
||||||
|
} else {
|
||||||
|
return node.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeValue(node) {
|
||||||
|
if (node.callee) {
|
||||||
|
// This is a CallExpression node.
|
||||||
|
// We get the position of the function name.
|
||||||
|
return getNodeValue(node.callee)
|
||||||
|
} else if (node.property) {
|
||||||
|
// This is a MemberExpression node.
|
||||||
|
// We get the value of the property.
|
||||||
|
return node.property.value
|
||||||
|
} else {
|
||||||
|
if (node.type === "Identifier") {
|
||||||
|
var query = { type: "definition", start: node.start, end: node.end }
|
||||||
|
var expr = tern.findQueryExpr(file, query)
|
||||||
|
var type = infer.expressionType(expr)
|
||||||
|
var objExpr = type.getType()
|
||||||
|
if (objExpr && objExpr.originNode) return getNodeValue(objExpr.originNode)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return node.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosition(node) {
|
||||||
|
if (node.callee) {
|
||||||
|
// This is a CallExpression node.
|
||||||
|
// We get the position of the function name.
|
||||||
|
return getPosition(node.callee)
|
||||||
|
}
|
||||||
|
if (node.property) {
|
||||||
|
// This is a MemberExpression node.
|
||||||
|
// We get the position of the property.
|
||||||
|
return node.property
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeName(type) {
|
||||||
|
if (!type) return "Unknown type"
|
||||||
|
if (type.types) {
|
||||||
|
// multiple types
|
||||||
|
var types = type.types,
|
||||||
|
s = ""
|
||||||
|
for (var i = 0; i < types.length; i++) {
|
||||||
|
if (i > 0) s += "|"
|
||||||
|
var t = getTypeName(types[i])
|
||||||
|
if (t != "Unknown type") s += t
|
||||||
|
}
|
||||||
|
return s == "" ? "Unknown type" : s
|
||||||
|
}
|
||||||
|
if (type.name) {
|
||||||
|
return type.name
|
||||||
|
}
|
||||||
|
return type.proto ? type.proto.name : "Unknown type"
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProto(expectedType, name) {
|
||||||
|
if (!expectedType) return false
|
||||||
|
if (!expectedType.proto) return false
|
||||||
|
return expectedType.proto.name === name
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRegexExpected(expectedType) {
|
||||||
|
return hasProto(expectedType, "RegExp.prototype")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmptyType(val) {
|
||||||
|
return !val || (val.types && val.types.length == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareType(expected, actual) {
|
||||||
|
if (isEmptyType(expected) || isEmptyType(actual)) return true
|
||||||
|
if (expected.types) {
|
||||||
|
for (var i = 0; i < expected.types.length; i++) {
|
||||||
|
if (actual.types) {
|
||||||
|
for (var j = 0; j < actual.types.length; j++) {
|
||||||
|
if (compareType(expected.types[i], actual.types[j])) return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (compareType(expected.types[i], actual.getType())) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else if (actual.types) {
|
||||||
|
for (var i = 0; i < actual.types.length; i++) {
|
||||||
|
if (compareType(expected.getType(), actual.types[i])) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var expectedType = expected.getType(),
|
||||||
|
actualType = actual.getType()
|
||||||
|
if (!expectedType || !actualType) return true
|
||||||
|
var currentProto = actualType.proto
|
||||||
|
while (currentProto) {
|
||||||
|
if (expectedType.proto && expectedType.proto.name === currentProto.name) return true
|
||||||
|
currentProto = currentProto.proto
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPropsInObject(node, expectedArg, actualObj, invalidArgument) {
|
||||||
|
var properties = node.properties,
|
||||||
|
expectedObj = expectedArg.getType()
|
||||||
|
for (var i = 0; i < properties.length; i++) {
|
||||||
|
var property = properties[i],
|
||||||
|
key = property.key,
|
||||||
|
prop = key && key.name,
|
||||||
|
value = property.value
|
||||||
|
if (prop) {
|
||||||
|
var expectedType = expectedObj.hasProp(prop)
|
||||||
|
if (!expectedType) {
|
||||||
|
// key doesn't exists
|
||||||
|
addMessage(
|
||||||
|
key,
|
||||||
|
"Invalid property at " +
|
||||||
|
(i + 1) +
|
||||||
|
": " +
|
||||||
|
prop +
|
||||||
|
" is not a property in " +
|
||||||
|
getTypeName(expectedArg),
|
||||||
|
invalidArgument.severity
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// test that each object literal prop is the correct type
|
||||||
|
var actualType = actualObj.props[prop]
|
||||||
|
if (!compareType(expectedType, actualType)) {
|
||||||
|
addMessage(
|
||||||
|
value,
|
||||||
|
"Invalid property at " +
|
||||||
|
(i + 1) +
|
||||||
|
": cannot convert from " +
|
||||||
|
getTypeName(actualType) +
|
||||||
|
" to " +
|
||||||
|
getTypeName(expectedType),
|
||||||
|
invalidArgument.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkItemInArray(node, expectedArg, state, invalidArgument) {
|
||||||
|
var elements = node.elements,
|
||||||
|
expectedType = expectedArg.hasProp("<i>")
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
var elt = elements[i],
|
||||||
|
actualType = infer.expressionType({ node: elt, state: state })
|
||||||
|
if (!compareType(expectedType, actualType)) {
|
||||||
|
addMessage(
|
||||||
|
elt,
|
||||||
|
"Invalid item at " +
|
||||||
|
(i + 1) +
|
||||||
|
": cannot convert from " +
|
||||||
|
getTypeName(actualType) +
|
||||||
|
" to " +
|
||||||
|
getTypeName(expectedType),
|
||||||
|
invalidArgument.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObjectLiteral(type) {
|
||||||
|
var objType = type.getObjType()
|
||||||
|
return objType && objType.proto && objType.proto.name == "Object.prototype"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFunctionLint(fnType) {
|
||||||
|
if (fnType.lint) return fnType.lint
|
||||||
|
if (fnType.metaData) {
|
||||||
|
fnType.lint = getLint(fnType.metaData["!lint"])
|
||||||
|
return fnType.lint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFunctionType(type) {
|
||||||
|
if (type.types) {
|
||||||
|
for (var i = 0; i < type.types.length; i++) {
|
||||||
|
if (isFunctionType(type.types[i])) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type.proto && type.proto.name == "Function.prototype"
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCallExpression(node, state, c) {
|
||||||
|
var notAFunctionRule = getRule("NotAFunction"),
|
||||||
|
invalidArgument = getRule("InvalidArgument")
|
||||||
|
if (!notAFunctionRule && !invalidArgument) return
|
||||||
|
var type = infer.expressionType({ node: node.callee, state: state })
|
||||||
|
if (!type.isEmpty()) {
|
||||||
|
// If type.isEmpty(), it is handled by MemberExpression/Identifier already.
|
||||||
|
|
||||||
|
// An expression can have multiple possible (guessed) types.
|
||||||
|
// If one of them is a function, type.getFunctionType() will return it.
|
||||||
|
var fnType = type.getFunctionType()
|
||||||
|
if (fnType == null) {
|
||||||
|
if (notAFunctionRule && !isFunctionType(type))
|
||||||
|
addMessage(
|
||||||
|
node,
|
||||||
|
"'" + getNodeName(node) + "' is not a function",
|
||||||
|
notAFunctionRule.severity
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var fnLint = getFunctionLint(fnType)
|
||||||
|
var continueLint = fnLint ? fnLint(node, addMessage, getRule) : true
|
||||||
|
if (continueLint && fnType.args) {
|
||||||
|
// validate parameters of the function
|
||||||
|
if (!invalidArgument) return
|
||||||
|
var actualArgs = node.arguments
|
||||||
|
if (!actualArgs) return
|
||||||
|
var expectedArgs = fnType.args
|
||||||
|
for (var i = 0; i < expectedArgs.length; i++) {
|
||||||
|
var expectedArg = expectedArgs[i]
|
||||||
|
if (actualArgs.length > i) {
|
||||||
|
var actualNode = actualArgs[i]
|
||||||
|
if (isRegexExpected(expectedArg.getType())) {
|
||||||
|
var value = getNodeValue(actualNode)
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
var regex = new RegExp(value)
|
||||||
|
} catch (e) {
|
||||||
|
addMessage(
|
||||||
|
actualNode,
|
||||||
|
"Invalid argument at " + (i + 1) + ": " + e,
|
||||||
|
invalidArgument.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var actualArg = infer.expressionType({ node: actualNode, state: state })
|
||||||
|
// if actual type is an Object literal and expected type is an object, we ignore
|
||||||
|
// the comparison type since object literal properties validation is done inside "ObjectExpression".
|
||||||
|
if (!(expectedArg.getObjType() && isObjectLiteral(actualArg))) {
|
||||||
|
if (!compareType(expectedArg, actualArg)) {
|
||||||
|
addMessage(
|
||||||
|
actualNode,
|
||||||
|
"Invalid argument at " +
|
||||||
|
(i + 1) +
|
||||||
|
": cannot convert from " +
|
||||||
|
getTypeName(actualArg) +
|
||||||
|
" to " +
|
||||||
|
getTypeName(expectedArg),
|
||||||
|
invalidArgument.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAssignement(nodeLeft, nodeRight, rule, state) {
|
||||||
|
if (!nodeLeft || !nodeRight) return
|
||||||
|
if (!rule) return
|
||||||
|
var leftType = infer.expressionType({ node: nodeLeft, state: state }),
|
||||||
|
rightType = infer.expressionType({ node: nodeRight, state: state })
|
||||||
|
if (!compareType(leftType, rightType)) {
|
||||||
|
addMessage(
|
||||||
|
nodeRight,
|
||||||
|
"Type mismatch: cannot convert from " +
|
||||||
|
getTypeName(leftType) +
|
||||||
|
" to " +
|
||||||
|
getTypeName(rightType),
|
||||||
|
rule.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDeclaration(node, state, c) {
|
||||||
|
function isUsedVariable(varNode, varState, file, srv) {
|
||||||
|
var name = varNode.name
|
||||||
|
|
||||||
|
for (var scope = varState; scope && !(name in scope.props); scope = scope.prev) {}
|
||||||
|
if (!scope) return false
|
||||||
|
|
||||||
|
var hasRef = false
|
||||||
|
function searchRef(file) {
|
||||||
|
return function (node, scopeHere) {
|
||||||
|
if (node != varNode) {
|
||||||
|
hasRef = true
|
||||||
|
throw new Error() // throw an error to stop the search.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (scope.node) {
|
||||||
|
// local scope
|
||||||
|
infer.findRefs(scope.node, scope, name, scope, searchRef(file))
|
||||||
|
} else {
|
||||||
|
// global scope
|
||||||
|
infer.findRefs(file.ast, file.scope, name, scope, searchRef(file))
|
||||||
|
for (var i = 0; i < srv.files.length && !hasRef; ++i) {
|
||||||
|
var cur = srv.files[i]
|
||||||
|
if (cur != file) infer.findRefs(cur.ast, cur.scope, name, scope, searchRef(cur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return hasRef
|
||||||
|
}
|
||||||
|
|
||||||
|
var unusedRule = getRule("UnusedVariable"),
|
||||||
|
mismatchRule = getRule("TypeMismatch")
|
||||||
|
if (!unusedRule && !mismatchRule) return
|
||||||
|
switch (node.type) {
|
||||||
|
case "VariableDeclaration":
|
||||||
|
for (var i = 0; i < node.declarations.length; ++i) {
|
||||||
|
var decl = node.declarations[i],
|
||||||
|
varNode = decl.id
|
||||||
|
if (varNode.name != "✖") {
|
||||||
|
// unused variable
|
||||||
|
if (unusedRule && !isUsedVariable(varNode, state, file, server))
|
||||||
|
addMessage(
|
||||||
|
varNode,
|
||||||
|
"Unused variable '" + getNodeName(varNode) + "'",
|
||||||
|
unusedRule.severity
|
||||||
|
)
|
||||||
|
// type mismatch?
|
||||||
|
if (mismatchRule) validateAssignement(varNode, decl.init, mismatchRule, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "FunctionDeclaration":
|
||||||
|
if (unusedRule) {
|
||||||
|
var varNode = node.id
|
||||||
|
if (varNode.name != "✖" && !isUsedVariable(varNode, state, file, server))
|
||||||
|
addMessage(
|
||||||
|
varNode,
|
||||||
|
"Unused function '" + getNodeName(varNode) + "'",
|
||||||
|
unusedRule.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrType(type) {
|
||||||
|
if (type instanceof infer.Arr) {
|
||||||
|
return type.getObjType()
|
||||||
|
} else if (type.types) {
|
||||||
|
for (var i = 0; i < type.types.length; i++) {
|
||||||
|
if (getArrType(type.types[i])) return type.types[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var visitors = {
|
||||||
|
VariableDeclaration: validateDeclaration,
|
||||||
|
FunctionDeclaration: validateDeclaration,
|
||||||
|
ReturnStatement: function (node, state, c) {
|
||||||
|
if (!node.argument) return
|
||||||
|
var rule = getRule("MixedReturnTypes")
|
||||||
|
if (!rule) return
|
||||||
|
if (state.fnType && state.fnType.retval) {
|
||||||
|
var actualType = infer.expressionType({ node: node.argument, state: state }),
|
||||||
|
expectedType = state.fnType.retval
|
||||||
|
if (!compareType(expectedType, actualType)) {
|
||||||
|
addMessage(
|
||||||
|
node,
|
||||||
|
"Invalid return type : cannot convert from " +
|
||||||
|
getTypeName(actualType) +
|
||||||
|
" to " +
|
||||||
|
getTypeName(expectedType),
|
||||||
|
rule.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Detects expressions of the form `object.property`
|
||||||
|
MemberExpression: function (node, state, c) {
|
||||||
|
var rule = getRule("UnknownProperty")
|
||||||
|
if (!rule) return
|
||||||
|
var prop = node.property && node.property.name
|
||||||
|
if (!prop || prop == "✖") return
|
||||||
|
var type = infer.expressionType({ node: node, state: state })
|
||||||
|
var parentType = infer.expressionType({ node: node.object, state: state })
|
||||||
|
|
||||||
|
if (node.computed) {
|
||||||
|
// Bracket notation.
|
||||||
|
// Until we figure out how to handle these properly, we ignore these nodes.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentType.isEmpty() && type.isEmpty()) {
|
||||||
|
// The type of the property cannot be determined, which means
|
||||||
|
// that the property probably doesn't exist.
|
||||||
|
|
||||||
|
// We only do this check if the parent type is known,
|
||||||
|
// otherwise we will generate errors for an entire chain of unknown
|
||||||
|
// properties.
|
||||||
|
|
||||||
|
// Also, the expression may be valid even if the parent type is unknown,
|
||||||
|
// since the inference engine cannot detect the type in all cases.
|
||||||
|
|
||||||
|
var propertyDefined = false
|
||||||
|
|
||||||
|
// In some cases the type is unknown, even if the property is defined
|
||||||
|
if (parentType.types) {
|
||||||
|
// We cannot use parentType.hasProp or parentType.props - in the case of an AVal,
|
||||||
|
// this may contain properties that are not really defined.
|
||||||
|
parentType.types.forEach(function (potentialType) {
|
||||||
|
// Obj#hasProp checks the prototype as well
|
||||||
|
if (typeof potentialType.hasProp == "function" && potentialType.hasProp(prop, true)) {
|
||||||
|
propertyDefined = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyDefined) {
|
||||||
|
addMessage(node, "Unknown property '" + getNodeName(node) + "'", rule.severity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Detects top-level identifiers, e.g. the object in
|
||||||
|
// `object.property` or just `object`.
|
||||||
|
Identifier: function (node, state, c) {
|
||||||
|
var rule = getRule("UnknownIdentifier")
|
||||||
|
if (!rule) return
|
||||||
|
var type = infer.expressionType({ node: node, state: state })
|
||||||
|
|
||||||
|
if (type.originNode != null || type.origin != null) {
|
||||||
|
// The node is defined somewhere (could be this node),
|
||||||
|
// regardless of whether or not the type is known.
|
||||||
|
} else if (type.isEmpty()) {
|
||||||
|
// The type of the identifier cannot be determined,
|
||||||
|
// and the origin is unknown.
|
||||||
|
addMessage(node, "Unknown identifier '" + getNodeName(node) + "'", rule.severity)
|
||||||
|
} else {
|
||||||
|
// Even though the origin node is unknown, the type is known.
|
||||||
|
// This is typically the case for built-in identifiers (e.g. window or document).
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Detects function calls.
|
||||||
|
// `node.callee` is the expression (Identifier or MemberExpression)
|
||||||
|
// the is called as a function.
|
||||||
|
NewExpression: validateCallExpression,
|
||||||
|
CallExpression: validateCallExpression,
|
||||||
|
AssignmentExpression: function (node, state, c) {
|
||||||
|
var rule = getRule("TypeMismatch")
|
||||||
|
validateAssignement(node.left, node.right, rule, state)
|
||||||
|
},
|
||||||
|
ObjectExpression: function (node, state, c) {
|
||||||
|
// validate properties of the object literal
|
||||||
|
var rule = getRule("ObjectLiteral")
|
||||||
|
if (!rule) return
|
||||||
|
var actualType = node.objType
|
||||||
|
var ctxType = infer.typeFromContext(file.ast, { node: node, state: state }),
|
||||||
|
expectedType = null
|
||||||
|
if (ctxType instanceof infer.Obj) {
|
||||||
|
expectedType = ctxType.getObjType()
|
||||||
|
} else if (ctxType && ctxType.makeupType) {
|
||||||
|
var objType = ctxType.makeupType()
|
||||||
|
if (objType && objType.getObjType()) {
|
||||||
|
expectedType = objType.getObjType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expectedType && expectedType != actualType) {
|
||||||
|
// expected type is known. Ex: config object of RequireJS
|
||||||
|
checkPropsInObject(node, expectedType, actualType, rule)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ArrayExpression: function (node, state, c) {
|
||||||
|
// validate elements of the Arrray
|
||||||
|
var rule = getRule("Array")
|
||||||
|
if (!rule) return
|
||||||
|
//var actualType = infer.expressionType({node: node, state: state});
|
||||||
|
var ctxType = infer.typeFromContext(file.ast, { node: node, state: state }),
|
||||||
|
expectedType = getArrType(ctxType)
|
||||||
|
if (expectedType /*&& expectedType != actualType*/) {
|
||||||
|
// expected type is known. Ex: config object of RequireJS
|
||||||
|
checkItemInArray(node, expectedType, state, rule)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ImportDeclaration: function (node, state, c) {
|
||||||
|
// Validate ES6 modules from + specifiers
|
||||||
|
var rule = getRule("ES6Modules")
|
||||||
|
if (!rule) return
|
||||||
|
var me = infer.cx().parent.mod.modules
|
||||||
|
if (!me) return // tern plugin modules.js is not loaded
|
||||||
|
var source = node.source
|
||||||
|
if (!source) return
|
||||||
|
// Validate ES6 modules "from"
|
||||||
|
var modType = me.getModType(source)
|
||||||
|
if (!modType) {
|
||||||
|
addMessage(source, "Invalid modules from '" + source.value + "'", rule.severity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Validate ES6 modules "specifiers"
|
||||||
|
var specifiers = node.specifiers,
|
||||||
|
specifier
|
||||||
|
if (!specifiers) return
|
||||||
|
for (var i = 0; i < specifiers.length; i++) {
|
||||||
|
var specifier = specifiers[i],
|
||||||
|
imported = specifier.imported
|
||||||
|
if (imported) {
|
||||||
|
var name = imported.name
|
||||||
|
if (!modType.hasProp(name))
|
||||||
|
addMessage(
|
||||||
|
imported,
|
||||||
|
"Invalid modules specifier '" + getNodeName(imported) + "'",
|
||||||
|
rule.severity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return visitors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from infer.searchVisitor.
|
||||||
|
// Record the scope and pass it through in the state.
|
||||||
|
// VariableDeclaration in infer.searchVisitor breaks things for us.
|
||||||
|
var scopeVisitor = walk.make({
|
||||||
|
Function: function (node, _st, c) {
|
||||||
|
var scope = node.scope
|
||||||
|
if (node.id) c(node.id, scope)
|
||||||
|
for (var i = 0; i < node.params.length; ++i) c(node.params[i], scope)
|
||||||
|
c(node.body, scope, "ScopeBody")
|
||||||
|
},
|
||||||
|
Statement: function (node, st, c) {
|
||||||
|
c(node, node.scope || st)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate one file
|
||||||
|
|
||||||
|
export function validateFile(server, query, file) {
|
||||||
|
try {
|
||||||
|
var messages = [],
|
||||||
|
ast = file.ast,
|
||||||
|
state = file.scope
|
||||||
|
var visitors = makeVisitors(server, query, file, messages)
|
||||||
|
walk.simple(ast, visitors, infer.searchVisitor, state)
|
||||||
|
return { messages: messages }
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return { messages: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerTernLinter() {
|
||||||
|
tern.defineQueryType("lint", {
|
||||||
|
takesFile: true,
|
||||||
|
run: function (server, query, file) {
|
||||||
|
return validateFile(server, query, file)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
tern.defineQueryType("lint-full", {
|
||||||
|
run: function (server, query) {
|
||||||
|
return validateFiles(server, query)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
tern.registerPlugin("lint", function (server, options) {
|
||||||
|
server._lint = {
|
||||||
|
rules: getRules(options),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
passes: {},
|
||||||
|
loadFirst: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the whole files of the server
|
||||||
|
|
||||||
|
export function validateFiles(server, query) {
|
||||||
|
try {
|
||||||
|
var messages = [],
|
||||||
|
files = server.files,
|
||||||
|
groupByFiles = query.groupByFiles == true
|
||||||
|
for (var i = 0; i < files.length; ++i) {
|
||||||
|
var messagesFile = groupByFiles ? [] : messages,
|
||||||
|
file = files[i],
|
||||||
|
ast = file.ast,
|
||||||
|
state = file.scope
|
||||||
|
var visitors = makeVisitors(server, query, file, messagesFile)
|
||||||
|
walk.simple(ast, visitors, infer.searchVisitor, state)
|
||||||
|
if (groupByFiles) messages.push({ file: file.name, messages: messagesFile })
|
||||||
|
}
|
||||||
|
return { messages: messages }
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack)
|
||||||
|
return { messages: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lints = Object.create(null)
|
||||||
|
|
||||||
|
var getLint = (tern.getLint = function (name) {
|
||||||
|
if (!name) return null
|
||||||
|
return lints[name]
|
||||||
|
})
|
||||||
|
|
||||||
|
function getRules(options) {
|
||||||
|
var rules = {}
|
||||||
|
for (var ruleName in defaultRules) {
|
||||||
|
if (options && options.rules && options.rules[ruleName] && options.rules[ruleName].severity) {
|
||||||
|
if (options.rules[ruleName].severity != "none") rules[ruleName] = options.rules[ruleName]
|
||||||
|
} else {
|
||||||
|
rules[ruleName] = defaultRules[ruleName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRule(ruleName) {
|
||||||
|
const cx = infer.cx()
|
||||||
|
const server = cx.parent
|
||||||
|
const rules = server && server._lint && server._lint.rules ? server._lint.rules : defaultRules
|
||||||
|
return rules[ruleName]
|
||||||
|
}
|
||||||
@@ -326,6 +326,12 @@ export default {
|
|||||||
use: { loader: "raw-loader" },
|
use: { loader: "raw-loader" },
|
||||||
exclude: /(node_modules)/,
|
exclude: /(node_modules)/,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.mjs$/,
|
||||||
|
include: /node_modules/,
|
||||||
|
type: "javascript/auto"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parallel: true,
|
parallel: true,
|
||||||
|
|||||||
132
package-lock.json
generated
132
package-lock.json
generated
@@ -3810,9 +3810,9 @@
|
|||||||
"integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
|
"integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
|
||||||
},
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "6.4.2",
|
"version": "8.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz",
|
||||||
"integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
|
"integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ=="
|
||||||
},
|
},
|
||||||
"acorn-globals": {
|
"acorn-globals": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
@@ -3829,6 +3829,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3838,6 +3844,21 @@
|
|||||||
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"acorn-loose": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHhXoiF0Uch3IqsrnPpWwCtiv5PYvipTpT1k9lDMgQVVYc9iDuSl5zdJV358aI8twfHCYMFBRVYvAVki9wC/ng==",
|
||||||
|
"requires": {
|
||||||
|
"acorn": "^6.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"acorn-node": {
|
"acorn-node": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
|
||||||
@@ -3854,13 +3875,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
"acorn-walk": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
||||||
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
|
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-zn/7dYtoTVkG4EoMU55QlQU4F+m+T7Kren6Vj3C2DapWPnakG/DL9Ns5aPAPW5Ixd3uxXrV/BoMKKVFIazPcdg=="
|
||||||
},
|
},
|
||||||
"agent-base": {
|
"agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
@@ -15687,6 +15714,89 @@
|
|||||||
"supports-hyperlinks": "^2.0.0"
|
"supports-hyperlinks": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tern": {
|
||||||
|
"version": "0.24.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tern/-/tern-0.24.3.tgz",
|
||||||
|
"integrity": "sha512-Z8uvtdWIlFn1GWy0HW5FhZ8VDryZwoJUdnjZU25C7/PBOltLIn1uv+WF3rVq6S1761YbsmbZYRP/l0ZJBCkvrw==",
|
||||||
|
"requires": {
|
||||||
|
"acorn": "^6.0.0",
|
||||||
|
"acorn-loose": "^6.0.0",
|
||||||
|
"acorn-walk": "^6.0.0",
|
||||||
|
"enhanced-resolve": "^2.2.2",
|
||||||
|
"glob": "^7.1.1",
|
||||||
|
"minimatch": "^3.0.3",
|
||||||
|
"resolve-from": "2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
|
||||||
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
|
||||||
|
},
|
||||||
|
"enhanced-resolve": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-oRXDJQS2MC6Fp2Jp16V8zdli41k=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"memory-fs": "^0.3.0",
|
||||||
|
"object-assign": "^4.0.1",
|
||||||
|
"tapable": "^0.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory-fs": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=",
|
||||||
|
"requires": {
|
||||||
|
"errno": "^0.1.3",
|
||||||
|
"readable-stream": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||||
|
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve-from": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tapable": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"terser": {
|
"terser": {
|
||||||
"version": "4.8.0",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
|
||||||
@@ -16987,6 +17097,11 @@
|
|||||||
"webpack-sources": "^1.4.1"
|
"webpack-sources": "^1.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"acorn": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
|
||||||
|
},
|
||||||
"braces": {
|
"braces": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||||
@@ -17253,6 +17368,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
||||||
},
|
},
|
||||||
|
"acorn-walk": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
|
||||||
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
"@nuxtjs/sitemap": "^2.4.0",
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
"@nuxtjs/toast": "^3.3.1",
|
"@nuxtjs/toast": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
|
"acorn": "^8.0.4",
|
||||||
|
"acorn-walk": "^8.0.1",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"firebase": "^8.2.3",
|
"firebase": "^8.2.3",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"socket.io-client": "^3.1.0",
|
"socket.io-client": "^3.1.0",
|
||||||
"socketio-wildcard": "^2.0.0",
|
"socketio-wildcard": "^2.0.0",
|
||||||
|
"tern": "^0.24.3",
|
||||||
"v-tooltip": "^2.1.2",
|
"v-tooltip": "^2.1.2",
|
||||||
"vuejs-auto-complete": "^0.9.0",
|
"vuejs-auto-complete": "^0.9.0",
|
||||||
"vuex-persist": "^3.1.3",
|
"vuex-persist": "^3.1.3",
|
||||||
|
|||||||
@@ -507,6 +507,7 @@
|
|||||||
useWorker: false,
|
useWorker: false,
|
||||||
}"
|
}"
|
||||||
styles="rounded-b-lg"
|
styles="rounded-b-lg"
|
||||||
|
completeMode="pre"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -548,6 +549,7 @@
|
|||||||
useWorker: false,
|
useWorker: false,
|
||||||
}"
|
}"
|
||||||
styles="rounded-b-lg"
|
styles="rounded-b-lg"
|
||||||
|
completeMode="test"
|
||||||
/>
|
/>
|
||||||
<div v-if="testReports.length !== 0">
|
<div v-if="testReports.length !== 0">
|
||||||
<div class="row-wrapper">
|
<div class="row-wrapper">
|
||||||
|
|||||||
Reference in New Issue
Block a user