ternlint.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. import infer from "tern/lib/infer"
  2. import tern from "tern/lib/tern"
  3. const walk = require("acorn-walk")
  4. var defaultRules = {
  5. UnknownProperty: { severity: "warning" },
  6. UnknownIdentifier: { severity: "warning" },
  7. NotAFunction: { severity: "error" },
  8. InvalidArgument: { severity: "error" },
  9. UnusedVariable: { severity: "warning" },
  10. UnknownModule: { severity: "error" },
  11. MixedReturnTypes: { severity: "warning" },
  12. ObjectLiteral: { severity: "error" },
  13. TypeMismatch: { severity: "warning" },
  14. Array: { severity: "error" },
  15. ES6Modules: { severity: "error" },
  16. }
  17. function makeVisitors(server, query, file, messages) {
  18. function addMessage(node, msg, severity) {
  19. var error = makeError(node, msg, severity)
  20. messages.push(error)
  21. }
  22. function makeError(node, msg, severity) {
  23. var pos = getPosition(node)
  24. var error = {
  25. message: msg,
  26. from: tern.outputPos(query, file, pos.start),
  27. to: tern.outputPos(query, file, pos.end),
  28. severity: severity,
  29. }
  30. if (query.lineNumber) {
  31. error.lineNumber = query.lineCharPositions
  32. ? error.from.line
  33. : tern.outputPos({ lineCharPositions: true }, file, pos.start).line
  34. }
  35. if (!query.groupByFiles) error.file = file.name
  36. return error
  37. }
  38. function getNodeName(node) {
  39. if (node.callee) {
  40. // This is a CallExpression node.
  41. // We get the position of the function name.
  42. return getNodeName(node.callee)
  43. } else if (node.property) {
  44. // This is a MemberExpression node.
  45. // We get the name of the property.
  46. return node.property.name
  47. } else {
  48. return node.name
  49. }
  50. }
  51. function getNodeValue(node) {
  52. if (node.callee) {
  53. // This is a CallExpression node.
  54. // We get the position of the function name.
  55. return getNodeValue(node.callee)
  56. } else if (node.property) {
  57. // This is a MemberExpression node.
  58. // We get the value of the property.
  59. return node.property.value
  60. } else {
  61. if (node.type === "Identifier") {
  62. var query = { type: "definition", start: node.start, end: node.end }
  63. var expr = tern.findQueryExpr(file, query)
  64. var type = infer.expressionType(expr)
  65. var objExpr = type.getType()
  66. if (objExpr && objExpr.originNode) return getNodeValue(objExpr.originNode)
  67. return null
  68. }
  69. return node.value
  70. }
  71. }
  72. function getPosition(node) {
  73. if (node.callee) {
  74. // This is a CallExpression node.
  75. // We get the position of the function name.
  76. return getPosition(node.callee)
  77. }
  78. if (node.property) {
  79. // This is a MemberExpression node.
  80. // We get the position of the property.
  81. return node.property
  82. }
  83. return node
  84. }
  85. function getTypeName(type) {
  86. if (!type) return "Unknown type"
  87. if (type.types) {
  88. // multiple types
  89. var types = type.types,
  90. s = ""
  91. for (var i = 0; i < types.length; i++) {
  92. if (i > 0) s += "|"
  93. var t = getTypeName(types[i])
  94. if (t != "Unknown type") s += t
  95. }
  96. return s == "" ? "Unknown type" : s
  97. }
  98. if (type.name) {
  99. return type.name
  100. }
  101. return type.proto ? type.proto.name : "Unknown type"
  102. }
  103. function hasProto(expectedType, name) {
  104. if (!expectedType) return false
  105. if (!expectedType.proto) return false
  106. return expectedType.proto.name === name
  107. }
  108. function isRegexExpected(expectedType) {
  109. return hasProto(expectedType, "RegExp.prototype")
  110. }
  111. function isEmptyType(val) {
  112. return !val || (val.types && val.types.length == 0)
  113. }
  114. function compareType(expected, actual) {
  115. if (isEmptyType(expected) || isEmptyType(actual)) return true
  116. if (expected.types) {
  117. for (var i = 0; i < expected.types.length; i++) {
  118. if (actual.types) {
  119. for (var j = 0; j < actual.types.length; j++) {
  120. if (compareType(expected.types[i], actual.types[j])) return true
  121. }
  122. } else {
  123. if (compareType(expected.types[i], actual.getType())) return true
  124. }
  125. }
  126. return false
  127. } else if (actual.types) {
  128. for (var i = 0; i < actual.types.length; i++) {
  129. if (compareType(expected.getType(), actual.types[i])) return true
  130. }
  131. }
  132. var expectedType = expected.getType(),
  133. actualType = actual.getType()
  134. if (!expectedType || !actualType) return true
  135. var currentProto = actualType.proto
  136. while (currentProto) {
  137. if (expectedType.proto && expectedType.proto.name === currentProto.name) return true
  138. currentProto = currentProto.proto
  139. }
  140. return false
  141. }
  142. function checkPropsInObject(node, expectedArg, actualObj, invalidArgument) {
  143. var properties = node.properties,
  144. expectedObj = expectedArg.getType()
  145. for (var i = 0; i < properties.length; i++) {
  146. var property = properties[i],
  147. key = property.key,
  148. prop = key && key.name,
  149. value = property.value
  150. if (prop) {
  151. var expectedType = expectedObj.hasProp(prop)
  152. if (!expectedType) {
  153. // key doesn't exists
  154. addMessage(
  155. key,
  156. "Invalid property at " +
  157. (i + 1) +
  158. ": " +
  159. prop +
  160. " is not a property in " +
  161. getTypeName(expectedArg),
  162. invalidArgument.severity
  163. )
  164. } else {
  165. // test that each object literal prop is the correct type
  166. var actualType = actualObj.props[prop]
  167. if (!compareType(expectedType, actualType)) {
  168. addMessage(
  169. value,
  170. "Invalid property at " +
  171. (i + 1) +
  172. ": cannot convert from " +
  173. getTypeName(actualType) +
  174. " to " +
  175. getTypeName(expectedType),
  176. invalidArgument.severity
  177. )
  178. }
  179. }
  180. }
  181. }
  182. }
  183. function checkItemInArray(node, expectedArg, state, invalidArgument) {
  184. var elements = node.elements,
  185. expectedType = expectedArg.hasProp("<i>")
  186. for (var i = 0; i < elements.length; i++) {
  187. var elt = elements[i],
  188. actualType = infer.expressionType({ node: elt, state: state })
  189. if (!compareType(expectedType, actualType)) {
  190. addMessage(
  191. elt,
  192. "Invalid item at " +
  193. (i + 1) +
  194. ": cannot convert from " +
  195. getTypeName(actualType) +
  196. " to " +
  197. getTypeName(expectedType),
  198. invalidArgument.severity
  199. )
  200. }
  201. }
  202. }
  203. function isObjectLiteral(type) {
  204. var objType = type.getObjType()
  205. return objType && objType.proto && objType.proto.name == "Object.prototype"
  206. }
  207. function getFunctionLint(fnType) {
  208. if (fnType.lint) return fnType.lint
  209. if (fnType.metaData) {
  210. fnType.lint = getLint(fnType.metaData["!lint"])
  211. return fnType.lint
  212. }
  213. }
  214. function isFunctionType(type) {
  215. if (type.types) {
  216. for (var i = 0; i < type.types.length; i++) {
  217. if (isFunctionType(type.types[i])) return true
  218. }
  219. }
  220. return type.proto && type.proto.name == "Function.prototype"
  221. }
  222. function validateCallExpression(node, state, c) {
  223. var notAFunctionRule = getRule("NotAFunction"),
  224. invalidArgument = getRule("InvalidArgument")
  225. if (!notAFunctionRule && !invalidArgument) return
  226. var type = infer.expressionType({ node: node.callee, state: state })
  227. if (!type.isEmpty()) {
  228. // If type.isEmpty(), it is handled by MemberExpression/Identifier already.
  229. // An expression can have multiple possible (guessed) types.
  230. // If one of them is a function, type.getFunctionType() will return it.
  231. var fnType = type.getFunctionType()
  232. if (fnType == null) {
  233. if (notAFunctionRule && !isFunctionType(type))
  234. addMessage(
  235. node,
  236. "'" + getNodeName(node) + "' is not a function",
  237. notAFunctionRule.severity
  238. )
  239. return
  240. }
  241. var fnLint = getFunctionLint(fnType)
  242. var continueLint = fnLint ? fnLint(node, addMessage, getRule) : true
  243. if (continueLint && fnType.args) {
  244. // validate parameters of the function
  245. if (!invalidArgument) return
  246. var actualArgs = node.arguments
  247. if (!actualArgs) return
  248. var expectedArgs = fnType.args
  249. for (var i = 0; i < expectedArgs.length; i++) {
  250. var expectedArg = expectedArgs[i]
  251. if (actualArgs.length > i) {
  252. var actualNode = actualArgs[i]
  253. if (isRegexExpected(expectedArg.getType())) {
  254. var value = getNodeValue(actualNode)
  255. if (value) {
  256. try {
  257. var regex = new RegExp(value)
  258. } catch (e) {
  259. addMessage(
  260. actualNode,
  261. "Invalid argument at " + (i + 1) + ": " + e,
  262. invalidArgument.severity
  263. )
  264. }
  265. }
  266. } else {
  267. var actualArg = infer.expressionType({ node: actualNode, state: state })
  268. // if actual type is an Object literal and expected type is an object, we ignore
  269. // the comparison type since object literal properties validation is done inside "ObjectExpression".
  270. if (!(expectedArg.getObjType() && isObjectLiteral(actualArg))) {
  271. if (!compareType(expectedArg, actualArg)) {
  272. addMessage(
  273. actualNode,
  274. "Invalid argument at " +
  275. (i + 1) +
  276. ": cannot convert from " +
  277. getTypeName(actualArg) +
  278. " to " +
  279. getTypeName(expectedArg),
  280. invalidArgument.severity
  281. )
  282. }
  283. }
  284. }
  285. }
  286. }
  287. }
  288. }
  289. }
  290. function validateAssignement(nodeLeft, nodeRight, rule, state) {
  291. if (!nodeLeft || !nodeRight) return
  292. if (!rule) return
  293. var leftType = infer.expressionType({ node: nodeLeft, state: state }),
  294. rightType = infer.expressionType({ node: nodeRight, state: state })
  295. if (!compareType(leftType, rightType)) {
  296. addMessage(
  297. nodeRight,
  298. "Type mismatch: cannot convert from " +
  299. getTypeName(leftType) +
  300. " to " +
  301. getTypeName(rightType),
  302. rule.severity
  303. )
  304. }
  305. }
  306. function validateDeclaration(node, state, c) {
  307. function isUsedVariable(varNode, varState, file, srv) {
  308. var name = varNode.name
  309. for (var scope = varState; scope && !(name in scope.props); scope = scope.prev) {}
  310. if (!scope) return false
  311. var hasRef = false
  312. function searchRef(file) {
  313. return function (node, scopeHere) {
  314. if (node != varNode) {
  315. hasRef = true
  316. throw new Error() // throw an error to stop the search.
  317. }
  318. }
  319. }
  320. try {
  321. if (scope.node) {
  322. // local scope
  323. infer.findRefs(scope.node, scope, name, scope, searchRef(file))
  324. } else {
  325. // global scope
  326. infer.findRefs(file.ast, file.scope, name, scope, searchRef(file))
  327. for (var i = 0; i < srv.files.length && !hasRef; ++i) {
  328. var cur = srv.files[i]
  329. if (cur != file) infer.findRefs(cur.ast, cur.scope, name, scope, searchRef(cur))
  330. }
  331. }
  332. } catch (e) {}
  333. return hasRef
  334. }
  335. var unusedRule = getRule("UnusedVariable"),
  336. mismatchRule = getRule("TypeMismatch")
  337. if (!unusedRule && !mismatchRule) return
  338. switch (node.type) {
  339. case "VariableDeclaration":
  340. for (var i = 0; i < node.declarations.length; ++i) {
  341. var decl = node.declarations[i],
  342. varNode = decl.id
  343. if (varNode.name != "✖") {
  344. // unused variable
  345. if (unusedRule && !isUsedVariable(varNode, state, file, server))
  346. addMessage(
  347. varNode,
  348. "Unused variable '" + getNodeName(varNode) + "'",
  349. unusedRule.severity
  350. )
  351. // type mismatch?
  352. if (mismatchRule) validateAssignement(varNode, decl.init, mismatchRule, state)
  353. }
  354. }
  355. break
  356. case "FunctionDeclaration":
  357. if (unusedRule) {
  358. var varNode = node.id
  359. if (varNode.name != "✖" && !isUsedVariable(varNode, state, file, server))
  360. addMessage(
  361. varNode,
  362. "Unused function '" + getNodeName(varNode) + "'",
  363. unusedRule.severity
  364. )
  365. }
  366. break
  367. }
  368. }
  369. function getArrType(type) {
  370. if (type instanceof infer.Arr) {
  371. return type.getObjType()
  372. } else if (type.types) {
  373. for (var i = 0; i < type.types.length; i++) {
  374. if (getArrType(type.types[i])) return type.types[i]
  375. }
  376. }
  377. }
  378. var visitors = {
  379. VariableDeclaration: validateDeclaration,
  380. FunctionDeclaration: validateDeclaration,
  381. ReturnStatement: function (node, state, c) {
  382. if (!node.argument) return
  383. var rule = getRule("MixedReturnTypes")
  384. if (!rule) return
  385. if (state.fnType && state.fnType.retval) {
  386. var actualType = infer.expressionType({ node: node.argument, state: state }),
  387. expectedType = state.fnType.retval
  388. if (!compareType(expectedType, actualType)) {
  389. addMessage(
  390. node,
  391. "Invalid return type : cannot convert from " +
  392. getTypeName(actualType) +
  393. " to " +
  394. getTypeName(expectedType),
  395. rule.severity
  396. )
  397. }
  398. }
  399. },
  400. // Detects expressions of the form `object.property`
  401. MemberExpression: function (node, state, c) {
  402. var rule = getRule("UnknownProperty")
  403. if (!rule) return
  404. var prop = node.property && node.property.name
  405. if (!prop || prop == "✖") return
  406. var type = infer.expressionType({ node: node, state: state })
  407. var parentType = infer.expressionType({ node: node.object, state: state })
  408. if (node.computed) {
  409. // Bracket notation.
  410. // Until we figure out how to handle these properly, we ignore these nodes.
  411. return
  412. }
  413. if (!parentType.isEmpty() && type.isEmpty()) {
  414. // The type of the property cannot be determined, which means
  415. // that the property probably doesn't exist.
  416. // We only do this check if the parent type is known,
  417. // otherwise we will generate errors for an entire chain of unknown
  418. // properties.
  419. // Also, the expression may be valid even if the parent type is unknown,
  420. // since the inference engine cannot detect the type in all cases.
  421. var propertyDefined = false
  422. // In some cases the type is unknown, even if the property is defined
  423. if (parentType.types) {
  424. // We cannot use parentType.hasProp or parentType.props - in the case of an AVal,
  425. // this may contain properties that are not really defined.
  426. parentType.types.forEach(function (potentialType) {
  427. // Obj#hasProp checks the prototype as well
  428. if (typeof potentialType.hasProp == "function" && potentialType.hasProp(prop, true)) {
  429. propertyDefined = true
  430. }
  431. })
  432. }
  433. if (!propertyDefined) {
  434. addMessage(node, "Unknown property '" + getNodeName(node) + "'", rule.severity)
  435. }
  436. }
  437. },
  438. // Detects top-level identifiers, e.g. the object in
  439. // `object.property` or just `object`.
  440. Identifier: function (node, state, c) {
  441. var rule = getRule("UnknownIdentifier")
  442. if (!rule) return
  443. var type = infer.expressionType({ node: node, state: state })
  444. if (type.originNode != null || type.origin != null) {
  445. // The node is defined somewhere (could be this node),
  446. // regardless of whether or not the type is known.
  447. } else if (type.isEmpty()) {
  448. // The type of the identifier cannot be determined,
  449. // and the origin is unknown.
  450. addMessage(node, "Unknown identifier '" + getNodeName(node) + "'", rule.severity)
  451. } else {
  452. // Even though the origin node is unknown, the type is known.
  453. // This is typically the case for built-in identifiers (e.g. window or document).
  454. }
  455. },
  456. // Detects function calls.
  457. // `node.callee` is the expression (Identifier or MemberExpression)
  458. // the is called as a function.
  459. NewExpression: validateCallExpression,
  460. CallExpression: validateCallExpression,
  461. AssignmentExpression: function (node, state, c) {
  462. var rule = getRule("TypeMismatch")
  463. validateAssignement(node.left, node.right, rule, state)
  464. },
  465. ObjectExpression: function (node, state, c) {
  466. // validate properties of the object literal
  467. var rule = getRule("ObjectLiteral")
  468. if (!rule) return
  469. var actualType = node.objType
  470. var ctxType = infer.typeFromContext(file.ast, { node: node, state: state }),
  471. expectedType = null
  472. if (ctxType instanceof infer.Obj) {
  473. expectedType = ctxType.getObjType()
  474. } else if (ctxType && ctxType.makeupType) {
  475. var objType = ctxType.makeupType()
  476. if (objType && objType.getObjType()) {
  477. expectedType = objType.getObjType()
  478. }
  479. }
  480. if (expectedType && expectedType != actualType) {
  481. // expected type is known. Ex: config object of RequireJS
  482. checkPropsInObject(node, expectedType, actualType, rule)
  483. }
  484. },
  485. ArrayExpression: function (node, state, c) {
  486. // validate elements of the Arrray
  487. var rule = getRule("Array")
  488. if (!rule) return
  489. //var actualType = infer.expressionType({node: node, state: state});
  490. var ctxType = infer.typeFromContext(file.ast, { node: node, state: state }),
  491. expectedType = getArrType(ctxType)
  492. if (expectedType /*&& expectedType != actualType*/) {
  493. // expected type is known. Ex: config object of RequireJS
  494. checkItemInArray(node, expectedType, state, rule)
  495. }
  496. },
  497. ImportDeclaration: function (node, state, c) {
  498. // Validate ES6 modules from + specifiers
  499. var rule = getRule("ES6Modules")
  500. if (!rule) return
  501. var me = infer.cx().parent.mod.modules
  502. if (!me) return // tern plugin modules.js is not loaded
  503. var source = node.source
  504. if (!source) return
  505. // Validate ES6 modules "from"
  506. var modType = me.getModType(source)
  507. if (!modType) {
  508. addMessage(source, "Invalid modules from '" + source.value + "'", rule.severity)
  509. return
  510. }
  511. // Validate ES6 modules "specifiers"
  512. var specifiers = node.specifiers,
  513. specifier
  514. if (!specifiers) return
  515. for (var i = 0; i < specifiers.length; i++) {
  516. var specifier = specifiers[i],
  517. imported = specifier.imported
  518. if (imported) {
  519. var name = imported.name
  520. if (!modType.hasProp(name))
  521. addMessage(
  522. imported,
  523. "Invalid modules specifier '" + getNodeName(imported) + "'",
  524. rule.severity
  525. )
  526. }
  527. }
  528. },
  529. }
  530. return visitors
  531. }
  532. // Adapted from infer.searchVisitor.
  533. // Record the scope and pass it through in the state.
  534. // VariableDeclaration in infer.searchVisitor breaks things for us.
  535. var scopeVisitor = walk.make({
  536. Function: function (node, _st, c) {
  537. var scope = node.scope
  538. if (node.id) c(node.id, scope)
  539. for (var i = 0; i < node.params.length; ++i) c(node.params[i], scope)
  540. c(node.body, scope, "ScopeBody")
  541. },
  542. Statement: function (node, st, c) {
  543. c(node, node.scope || st)
  544. },
  545. })
  546. // Validate one file
  547. export function validateFile(server, query, file) {
  548. try {
  549. var messages = [],
  550. ast = file.ast,
  551. state = file.scope
  552. var visitors = makeVisitors(server, query, file, messages)
  553. walk.simple(ast, visitors, infer.searchVisitor, state)
  554. return { messages: messages }
  555. } catch (err) {
  556. console.error(err.stack)
  557. return { messages: [] }
  558. }
  559. }
  560. export function registerTernLinter() {
  561. tern.defineQueryType("lint", {
  562. takesFile: true,
  563. run: function (server, query, file) {
  564. return validateFile(server, query, file)
  565. },
  566. })
  567. tern.defineQueryType("lint-full", {
  568. run: function (server, query) {
  569. return validateFiles(server, query)
  570. },
  571. })
  572. tern.registerPlugin("lint", function (server, options) {
  573. server._lint = {
  574. rules: getRules(options),
  575. }
  576. return {
  577. passes: {},
  578. loadFirst: true,
  579. }
  580. })
  581. }
  582. // Validate the whole files of the server
  583. export function validateFiles(server, query) {
  584. try {
  585. var messages = [],
  586. files = server.files,
  587. groupByFiles = query.groupByFiles == true
  588. for (var i = 0; i < files.length; ++i) {
  589. var messagesFile = groupByFiles ? [] : messages,
  590. file = files[i],
  591. ast = file.ast,
  592. state = file.scope
  593. var visitors = makeVisitors(server, query, file, messagesFile)
  594. walk.simple(ast, visitors, infer.searchVisitor, state)
  595. if (groupByFiles) messages.push({ file: file.name, messages: messagesFile })
  596. }
  597. return { messages: messages }
  598. } catch (err) {
  599. console.error(err.stack)
  600. return { messages: [] }
  601. }
  602. }
  603. var lints = Object.create(null)
  604. var getLint = (tern.getLint = function (name) {
  605. if (!name) return null
  606. return lints[name]
  607. })
  608. function getRules(options) {
  609. var rules = {}
  610. for (var ruleName in defaultRules) {
  611. if (options && options.rules && options.rules[ruleName] && options.rules[ruleName].severity) {
  612. if (options.rules[ruleName].severity != "none") rules[ruleName] = options.rules[ruleName]
  613. } else {
  614. rules[ruleName] = defaultRules[ruleName]
  615. }
  616. }
  617. return rules
  618. }
  619. function getRule(ruleName) {
  620. const cx = infer.cx()
  621. const server = cx.parent
  622. const rules = server && server._lint && server._lint.rules ? server._lint.rules : defaultRules
  623. return rules[ruleName]
  624. }