solidity-ast
Advanced tools
Comparing version
@@ -0,3 +1,3 @@ | ||
import { ExtendedNodeType } from '../utils/is-node-type'; | ||
import type { ASTDereferencer } from '../utils'; | ||
import type { NodeType } from '../node'; | ||
import type { SolcOutput } from '../solc'; | ||
@@ -12,5 +12,5 @@ export declare function astDereferencer(solcOutput: SolcOutput): ASTDereferencer; | ||
readonly id: number; | ||
readonly nodeType: readonly NodeType[]; | ||
constructor(id: number, nodeType: readonly NodeType[]); | ||
readonly nodeType: readonly ExtendedNodeType[]; | ||
constructor(id: number, nodeType: readonly ExtendedNodeType[]); | ||
} | ||
//# sourceMappingURL=ast-dereferencer.d.ts.map |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ASTDereferencerError = exports.curry2 = exports.astDereferencer = void 0; | ||
const is_node_type_1 = require("../utils/is-node-type"); | ||
const find_all_1 = require("../utils/find-all"); | ||
const array_prototype_findlast_1 = __importDefault(require("array.prototype.findlast")); | ||
// An ASTDereferencer is a function that looks up an AST node given its id, in all of the source files involved in a | ||
@@ -11,32 +16,56 @@ // solc run. It will generally be used together with the AST property `referencedDeclaration` (found in Identifier, | ||
const asts = Array.from(Object.values(solcOutput.sources), s => s.ast).sort((a, b) => a.id - b.id); | ||
// To look for a given node we iterate over all nodes in all ASTs. As an optimization, we try to find the ideal first | ||
// candidate based on the observation that node ids are assigned in postorder, therefore a SourceUnit's own id is always | ||
// larger than that of the nodes in it. As a fallback mechanism in case this assumption breaks, if the node is not found | ||
// in the first one we proceed to check all ASTs. | ||
function* astCandidates(id) { | ||
const first = asts.find(a => (id <= a.id)); | ||
if (first) { | ||
yield first; | ||
} | ||
for (const ast of asts) { | ||
if (ast !== first) { | ||
yield ast; | ||
} | ||
} | ||
} | ||
function deref(nodeType, id) { | ||
if (!isArray(nodeType)) { | ||
nodeType = [nodeType]; | ||
} | ||
const cached = cache.get(id); | ||
if (cached) { | ||
if (nodeType.includes(cached.node.nodeType)) { | ||
if ((0, is_node_type_1.isNodeType)(nodeType, cached.node)) { | ||
return cached; | ||
} | ||
} | ||
for (const ast of astCandidates(id)) { | ||
for (const node of find_all_1.findAll(nodeType, ast)) { | ||
if (node.id === id) { | ||
const nodeWithSourceUnit = { node, sourceUnit: ast }; | ||
cache.set(id, nodeWithSourceUnit); | ||
else if (id >= 0) { | ||
// Node ids appear to be assigned in postorder. This means that a node's own id is always | ||
// larger than that of the nodes under it. We descend through the AST guided by this | ||
// assumption. Among a set of sibling nodes we choose the one with the smallest id that | ||
// is larger than the id we're looking for. | ||
let ast = asts.find(ast => (id <= ast.id)); | ||
let root = ast; | ||
while (root) { | ||
if (Array.isArray(root)) { | ||
root = root.find(n => n && (id <= n.id)); | ||
} | ||
else if (root.id === id) { | ||
break; | ||
} | ||
else { | ||
let next, nextId; | ||
for (const cand of Object.values(root)) { | ||
if (typeof cand !== "object") | ||
continue; | ||
const candId = Array.isArray(cand) ? (0, array_prototype_findlast_1.default)(cand, n => n)?.id : cand.id; | ||
if (candId === undefined) | ||
continue; | ||
if (id <= candId && (nextId === undefined || candId <= nextId)) { | ||
next = cand; | ||
nextId = candId; | ||
} | ||
} | ||
root = next; | ||
} | ||
} | ||
let found = root; | ||
// As a fallback mechanism in case the postorder assumption breaks, if the node is not found | ||
// we proceed to check all nodes in all ASTs. | ||
if (found === undefined) { | ||
outer: for (ast of asts) { | ||
for (const node of (0, find_all_1.findAll)(nodeType, ast)) { | ||
if (node.id === id) { | ||
found = node; | ||
break outer; | ||
} | ||
} | ||
} | ||
} | ||
if (found !== undefined) { | ||
const nodeWithSourceUnit = { node: found, sourceUnit: ast }; | ||
cache.set(id, nodeWithSourceUnit); | ||
if ((0, is_node_type_1.isNodeType)(nodeType, found)) { | ||
return nodeWithSourceUnit; | ||
@@ -46,2 +75,3 @@ } | ||
} | ||
nodeType = Array.isArray(nodeType) ? nodeType : [nodeType]; | ||
throw new ASTDereferencerError(id, nodeType); | ||
@@ -69,2 +99,4 @@ } | ||
class ASTDereferencerError extends Error { | ||
id; | ||
nodeType; | ||
constructor(id, nodeType) { | ||
@@ -71,0 +103,0 @@ super(`No node with id ${id} of type ${nodeType}`); |
@@ -11,3 +11,2 @@ "use strict"; | ||
function getSource(sourceId) { | ||
var _a, _b; | ||
if (sourceId in sources) { | ||
@@ -17,7 +16,7 @@ return sources[sourceId]; | ||
else { | ||
const sourcePath = (_a = Object.entries(solcOutput.sources).find(([, { id }]) => sourceId === id)) === null || _a === void 0 ? void 0 : _a[0]; | ||
const sourcePath = Object.entries(solcOutput.sources).find(([, { id }]) => sourceId === id)?.[0]; | ||
if (sourcePath === undefined) { | ||
throw new Error(`Source file not available`); | ||
} | ||
const content = (_b = solcInput.sources[sourcePath]) === null || _b === void 0 ? void 0 : _b.content; | ||
const content = solcInput.sources[sourcePath]?.content; | ||
const name = path_1.default.relative(basePath, sourcePath); | ||
@@ -24,0 +23,0 @@ if (content === undefined) { |
{ | ||
"name": "solidity-ast", | ||
"version": "0.4.49", | ||
"version": "0.4.50", | ||
"description": "Solidity AST schema and type definitions", | ||
@@ -27,2 +27,5 @@ "author": "Francisco Giordano <frangio.1@gmail.com>", | ||
}, | ||
"dependencies": { | ||
"array.prototype.findlast": "^1.2.2" | ||
}, | ||
"devDependencies": { | ||
@@ -69,6 +72,8 @@ "@types/lodash": "^4.14.155", | ||
"solc-0.8.19": "npm:solc@0.8.19", | ||
"solc-0.8.20": "npm:solc@0.8.20", | ||
"solc-0.8.21": "npm:solc@0.8.21", | ||
"typedoc": "^0.17.7", | ||
"typescript": "^4.2.4" | ||
"typescript": "^4.9.5" | ||
}, | ||
"license": "MIT" | ||
} |
@@ -85,2 +85,5 @@ # Solidity AST Types | ||
To enumerate all subnodes regardless of node type, `nodeType` can be `'*'` (a | ||
string with a single asterisk). | ||
### `astDereferencer(solcOutput) => (nodeType, id) => Node` | ||
@@ -115,2 +118,4 @@ | ||
If the node type is unknown you can specify `'*'` for `nodeType`. | ||
### `srcDecoder(solcInput, solcOutput, basePath = '.') => (node: Node) => string` | ||
@@ -117,0 +122,0 @@ |
@@ -0,1 +1,2 @@ | ||
import { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap } from '../utils/is-node-type'; | ||
import { findAll } from '../utils/find-all'; | ||
@@ -7,2 +8,4 @@ import type { ASTDereferencer, NodeWithSourceUnit } from '../utils'; | ||
import findLast from 'array.prototype.findlast'; | ||
// An ASTDereferencer is a function that looks up an AST node given its id, in all of the source files involved in a | ||
@@ -20,37 +23,59 @@ // solc run. It will generally be used together with the AST property `referencedDeclaration` (found in Identifier, | ||
// To look for a given node we iterate over all nodes in all ASTs. As an optimization, we try to find the ideal first | ||
// candidate based on the observation that node ids are assigned in postorder, therefore a SourceUnit's own id is always | ||
// larger than that of the nodes in it. As a fallback mechanism in case this assumption breaks, if the node is not found | ||
// in the first one we proceed to check all ASTs. | ||
function* astCandidates(id: number) { | ||
const first = asts.find(a => (id <= a.id)); | ||
if (first) { | ||
yield first; | ||
} | ||
for (const ast of asts) { | ||
if (ast !== first) { | ||
yield ast; | ||
} | ||
} | ||
} | ||
function deref<T extends NodeType>(nodeType: T | readonly T[], id: number): NodeWithSourceUnit<NodeTypeMap[T]>; | ||
function deref(nodeType: NodeType | readonly NodeType[], id: number): NodeWithSourceUnit { | ||
if (!isArray(nodeType)) { | ||
nodeType = [nodeType]; | ||
} | ||
function deref<T extends ExtendedNodeType>(nodeType: T | readonly T[], id: number): NodeWithSourceUnit<ExtendedNodeTypeMap[T]>; | ||
function deref(nodeType: ExtendedNodeType | readonly ExtendedNodeType[], id: number): NodeWithSourceUnit { | ||
const cached = cache.get(id); | ||
if (cached) { | ||
if (nodeType.includes(cached.node.nodeType)) { | ||
if (isNodeType(nodeType, cached.node)) { | ||
return cached; | ||
} | ||
} | ||
} else if (id >= 0) { | ||
// Node ids appear to be assigned in postorder. This means that a node's own id is always | ||
// larger than that of the nodes under it. We descend through the AST guided by this | ||
// assumption. Among a set of sibling nodes we choose the one with the smallest id that | ||
// is larger than the id we're looking for. | ||
for (const ast of astCandidates(id)) { | ||
for (const node of findAll(nodeType, ast)) { | ||
if (node.id === id) { | ||
const nodeWithSourceUnit = { node, sourceUnit: ast }; | ||
cache.set(id, nodeWithSourceUnit); | ||
let ast = asts.find(ast => (id <= ast.id)); | ||
let root: Node | Node[] | undefined = ast; | ||
while (root) { | ||
if (Array.isArray(root)) { | ||
root = root.find(n => n && (id <= n.id)); | ||
} else if (root.id === id) { | ||
break; | ||
} else { | ||
let next, nextId; | ||
for (const cand of Object.values(root)) { | ||
if (typeof cand !== "object") continue; | ||
const candId = Array.isArray(cand) ? findLast(cand, n => n)?.id : cand.id; | ||
if (candId === undefined) continue; | ||
if (id <= candId && (nextId === undefined || candId <= nextId)) { | ||
next = cand; | ||
nextId = candId; | ||
} | ||
} | ||
root = next; | ||
} | ||
} | ||
let found: Node | undefined = root; | ||
// As a fallback mechanism in case the postorder assumption breaks, if the node is not found | ||
// we proceed to check all nodes in all ASTs. | ||
if (found === undefined) { | ||
outer: for (ast of asts) { | ||
for (const node of findAll(nodeType, ast)) { | ||
if (node.id === id) { | ||
found = node; | ||
break outer; | ||
} | ||
} | ||
} | ||
} | ||
if (found !== undefined) { | ||
const nodeWithSourceUnit = { node: found, sourceUnit: ast! }; | ||
cache.set(id, nodeWithSourceUnit); | ||
if (isNodeType(nodeType, found)) { | ||
return nodeWithSourceUnit; | ||
@@ -61,2 +86,3 @@ } | ||
nodeType = Array.isArray(nodeType) ? nodeType : [nodeType]; | ||
throw new ASTDereferencerError(id, nodeType); | ||
@@ -96,5 +122,5 @@ } | ||
export class ASTDereferencerError extends Error { | ||
constructor(readonly id: number, readonly nodeType: readonly NodeType[]) { | ||
constructor(readonly id: number, readonly nodeType: readonly ExtendedNodeType[]) { | ||
super(`No node with id ${id} of type ${nodeType}`); | ||
} | ||
} |
@@ -63,2 +63,3 @@ /* tslint:disable */ | ||
}; | ||
experimentalSolidity?: boolean; | ||
license?: string | null; | ||
@@ -105,2 +106,6 @@ nodes: ( | ||
usedErrors?: number[]; | ||
usedEvents?: number[]; | ||
internalFunctionIDs?: { | ||
[k: string]: number | undefined; | ||
}; | ||
nodeType: "ContractDefinition"; | ||
@@ -569,3 +574,4 @@ } | ||
| "london" | ||
| "paris"; | ||
| "paris" | ||
| "shanghai"; | ||
externalReferences: { | ||
@@ -586,2 +592,3 @@ declaration: number; | ||
nodeType: "YulBlock"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -593,2 +600,3 @@ export interface YulAssignment { | ||
nodeType: "YulAssignment"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -600,2 +608,3 @@ export interface YulFunctionCall { | ||
nodeType: "YulFunctionCall"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -606,2 +615,3 @@ export interface YulIdentifier { | ||
nodeType: "YulIdentifier"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -614,2 +624,3 @@ export interface YulLiteralValue { | ||
nodeType: "YulLiteral"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -623,2 +634,3 @@ export interface YulLiteralHexValue { | ||
nodeType: "YulLiteral"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -628,2 +640,3 @@ export interface YulBreak { | ||
nodeType: "YulBreak"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -633,2 +646,3 @@ export interface YulContinue { | ||
nodeType: "YulContinue"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -639,2 +653,3 @@ export interface YulExpressionStatement { | ||
nodeType: "YulExpressionStatement"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -644,2 +659,3 @@ export interface YulLeave { | ||
nodeType: "YulLeave"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -653,2 +669,3 @@ export interface YulForLoop { | ||
nodeType: "YulForLoop"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -662,2 +679,3 @@ export interface YulFunctionDefinition { | ||
nodeType: "YulFunctionDefinition"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -669,2 +687,3 @@ export interface YulTypedName { | ||
nodeType: "YulTypedName"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -676,2 +695,3 @@ export interface YulIf { | ||
nodeType: "YulIf"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -683,2 +703,3 @@ export interface YulSwitch { | ||
nodeType: "YulSwitch"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -690,2 +711,3 @@ export interface YulCase { | ||
nodeType: "YulCase"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -697,2 +719,3 @@ export interface YulVariableDeclaration { | ||
nodeType: "YulVariableDeclaration"; | ||
nativeSrc?: SourceLocation; | ||
} | ||
@@ -782,2 +805,3 @@ export interface PlaceholderStatement { | ||
visibility: Visibility; | ||
documentation?: StructuredDocumentation | null; | ||
nodeType: "StructDefinition"; | ||
@@ -784,0 +808,0 @@ } |
@@ -5,7 +5,7 @@ import { SolcInput, SolcOutput } from './solc'; | ||
import { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap } from './utils/is-node-type'; | ||
export { isNodeType, ExtendedNodeType, ExtendedNodeTypeMap }; | ||
export { findAll } from './utils/find-all'; | ||
export function isNodeType<N extends Node, T extends NodeType>(nodeType: T | readonly T[]): (node: N) => node is N & NodeTypeMap[T]; | ||
export function isNodeType<N extends Node, T extends NodeType>(nodeType: T | readonly T[], node: N): node is N & NodeTypeMap[T]; | ||
export interface NodeWithSourceUnit<N extends Node = Node> { | ||
@@ -17,5 +17,5 @@ node: N; | ||
export interface ASTDereferencer { | ||
<T extends NodeType>(nodeType: T | readonly T[]): (id: number) => NodeTypeMap[T]; | ||
<T extends NodeType>(nodeType: T | readonly T[], id: number): NodeTypeMap[T]; | ||
withSourceUnit<T extends NodeType>(nodeType: T | readonly T[], id: number): NodeWithSourceUnit<NodeTypeMap[T]>; | ||
<T extends ExtendedNodeType>(nodeType: T | readonly T[]): (id: number) => ExtendedNodeTypeMap[T]; | ||
<T extends ExtendedNodeType>(nodeType: T | readonly T[], id: number): ExtendedNodeTypeMap[T]; | ||
withSourceUnit<T extends ExtendedNodeType>(nodeType: T | readonly T[], id: number): NodeWithSourceUnit<ExtendedNodeTypeMap[T]>; | ||
} | ||
@@ -27,3 +27,3 @@ | ||
readonly id: number; | ||
readonly nodeType: readonly NodeType[]; | ||
readonly nodeType: readonly ExtendedNodeType[]; | ||
} | ||
@@ -30,0 +30,0 @@ |
10
utils.js
@@ -1,9 +0,1 @@ | ||
function isNodeType(nodeType, node) { | ||
if (Array.isArray(nodeType)) { | ||
return nodeType.includes(node.nodeType); | ||
} else { | ||
return node.nodeType === nodeType; | ||
} | ||
} | ||
function curry2(fn) { | ||
@@ -19,3 +11,3 @@ return function (nodeType, ...args) { | ||
module.exports.isNodeType = curry2(isNodeType); | ||
module.exports.isNodeType = curry2(require('./utils/is-node-type').isNodeType); | ||
module.exports.findAll = curry2(require('./utils/find-all').findAll); | ||
@@ -22,0 +14,0 @@ |
@@ -1,8 +0,9 @@ | ||
import { Node, NodeType, NodeTypeMap, YulNode, YulNodeType, YulNodeTypeMap } from '../node'; | ||
import { Node, YulNode, YulNodeType, YulNodeTypeMap } from '../node'; | ||
import { ExtendedNodeType, ExtendedNodeTypeMap } from './is-node-type'; | ||
export function findAll<T extends NodeType>(nodeType: T | readonly T[]): (node: Node) => Generator<NodeTypeMap[T]>; | ||
export function findAll<T extends NodeType>(nodeType: T | readonly T[], node: Node, prune?: (node: Node) => boolean): Generator<NodeTypeMap[T]>; | ||
export function findAll<T extends ExtendedNodeType>(nodeType: T | readonly T[]): (node: Node) => Generator<ExtendedNodeTypeMap[T]>; | ||
export function findAll<T extends ExtendedNodeType>(nodeType: T | readonly T[], node: Node, prune?: (node: Node) => boolean): Generator<ExtendedNodeTypeMap[T]>; | ||
export function findAll<T extends NodeType | YulNodeType>(nodeType: T | readonly T[]): (node: Node | YulNode) => Generator<(NodeTypeMap & YulNodeTypeMap)[T]>; | ||
export function findAll<T extends NodeType | YulNodeType>(nodeType: T | readonly T[], node: Node | YulNode, prune?: (node: Node | YulNode) => boolean): Generator<(NodeTypeMap & YulNodeTypeMap)[T]>; | ||
export function findAll<T extends ExtendedNodeType | YulNodeType>(nodeType: T | readonly T[]): (node: Node | YulNode) => Generator<(ExtendedNodeTypeMap & YulNodeTypeMap)[T]>; | ||
export function findAll<T extends ExtendedNodeType | YulNodeType>(nodeType: T | readonly T[], node: Node | YulNode, prune?: (node: Node | YulNode) => boolean): Generator<(ExtendedNodeTypeMap & YulNodeTypeMap)[T]>; | ||
@@ -0,26 +1,45 @@ | ||
const { isNodeType } = require('./is-node-type'); | ||
const finder = require('../finder.json'); | ||
const nextPropsCache = new Map(); | ||
function* findAll(nodeType, node, prune) { | ||
if (!Array.isArray(nodeType)) { | ||
nodeType = [nodeType]; | ||
} | ||
let cache; | ||
if (prune && prune(node)) { | ||
return; | ||
if (Array.isArray(nodeType)) { | ||
const cacheKey = JSON.stringify(nodeType); | ||
cache = nextPropsCache.get(cacheKey); | ||
if (!cache) { | ||
cache = {}; | ||
nextPropsCache.set(cacheKey, cache); | ||
} | ||
} | ||
if (nodeType.includes(node.nodeType)) { | ||
yield node; | ||
} | ||
const queue = []; | ||
const push = node => queue.push({ node, props: getNextProps(nodeType, node.nodeType ?? '$other', cache) }); | ||
for (const prop of getNextProps(nodeType, node.nodeType)) { | ||
const member = node[prop]; | ||
if (Array.isArray(member)) { | ||
for (const sub2 of member) { | ||
if (sub2) { | ||
yield* findAll(nodeType, sub2, prune); | ||
push(node); | ||
for (let i = 0; i < queue.length; i++) { | ||
const { node, props } = queue[i]; | ||
if (typeof node !== 'object' || (prune && prune(node))) { | ||
continue; | ||
} | ||
if (isNodeType(nodeType, node)) { | ||
yield node; | ||
} | ||
for (let j = 0; j < props.length; j++) { | ||
const member = node[props[j]]; | ||
if (Array.isArray(member)) { | ||
for (const sub2 of member) { | ||
if (sub2) { | ||
push(sub2); | ||
} | ||
} | ||
} else if (member) { | ||
push(member); | ||
} | ||
} else if (member) { | ||
yield* findAll(nodeType, member, prune); | ||
} | ||
@@ -30,16 +49,10 @@ } | ||
const nextPropsCache = new WeakMap(); | ||
function getNextProps(wantedNodeTypes, currentNodeType) { | ||
function getNextProps(wantedNodeTypes, currentNodeType, cache) { | ||
if (typeof wantedNodeTypes === 'string') { | ||
return finder[wantedNodeType] ?? []; | ||
return finder[wantedNodeTypes][currentNodeType] ?? []; | ||
} | ||
let cache = nextPropsCache.get(wantedNodeTypes); | ||
if (!cache) { | ||
cache = {}; | ||
nextPropsCache.set(wantedNodeTypes, cache); | ||
} else if (currentNodeType in cache) { | ||
if (currentNodeType in cache) { | ||
return cache[currentNodeType]; | ||
} | ||
const next = new Set(); | ||
const next = []; | ||
for (const wantedNodeType of wantedNodeTypes) { | ||
@@ -49,3 +62,5 @@ const wantedFinder = finder[wantedNodeType]; | ||
for (const nextNodeType of wantedFinder[currentNodeType]) { | ||
next.add(nextNodeType); | ||
if (!next.includes(nextNodeType)) { | ||
next.push(nextNodeType); | ||
} | ||
} | ||
@@ -52,0 +67,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
235917
2.16%10928
2.94%135
3.85%1
Infinity%44
4.76%25
-16.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added